mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-30 01:12:05 +00:00
Merge branch 'main' into jules/testing-relay-health-12520607155456440581
This commit is contained in:
133 files changed
+11210
-2781
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 :: Required startup + artifacts + memory + issue capture
|
||||
- Memory retrieval seed for daily :: scheduler memory retrieval
|
||||
- Memory retrieval seed for daily :: scheduler memory retrieval
|
||||
+208
-271
@@ -1,36 +1,88 @@
|
||||
[
|
||||
[
|
||||
"638c2d47-3218-42f5-bc2d-e3adeb4626d0",
|
||||
"dc16f83d-c99b-4dc0-ac52-90bd36cf709b",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "638c2d47-3218-42f5-bc2d-e3adeb4626d0",
|
||||
"agent_id": "test-agent",
|
||||
"session_id": "test-run",
|
||||
"id": "dc16f83d-c99b-4dc0-ac52-90bd36cf709b",
|
||||
"agent_id": "ci-health-agent",
|
||||
"session_id": "session-mlr07hyw",
|
||||
"type": "batch_window",
|
||||
"content": "Store memory event A for daily :: scheduler memory store\nStore memory event B for daily :: scheduler memory store",
|
||||
"summary": "Store memory event A for daily :: scheduler memory store Store memory event B for daily :: scheduler memory store",
|
||||
"content": "Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture",
|
||||
"summary": "Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"daily",
|
||||
"store"
|
||||
"retrieve"
|
||||
],
|
||||
"importance": 0.55,
|
||||
"importance": 0.4,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771295950236,
|
||||
"last_seen": 1771295958659,
|
||||
"source": "scheduler-store",
|
||||
"created_at": 1771356975801,
|
||||
"last_seen": 1771356975809,
|
||||
"source": "scheduler-retrieve",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"5c458e0c-8239-46f9-99d1-b61cf05acc34",
|
||||
"cf3230aa-9e00-48f4-809f-a54fee5a0637",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "5c458e0c-8239-46f9-99d1-b61cf05acc34",
|
||||
"agent_id": "test-agent",
|
||||
"session_id": "test-run-2",
|
||||
"id": "cf3230aa-9e00-48f4-809f-a54fee5a0637",
|
||||
"agent_id": "ci-health-agent",
|
||||
"session_id": "session-mlr0b3uh",
|
||||
"type": "batch_window",
|
||||
"content": "# CI Health Update\n- Fixed bug in `src/lock-ops.mjs`: `getRelayFallbacks` is async but was called synchronously in `LockPublisher` constructor.\n- Fixed bug in `src/lock-ops.mjs`: `LockPublisher` used `maybeLogHealthSnapshot` which was undefined; updated to use `healthManager`.\n- Fixed bug in `src/cmd-check.mjs`: `queryLocksFn` was undefined; updated to use injected `queryLocksFn` or default `queryLocks`.\n- Ran `npm run build` to fix `test/build_verification.test.mjs`.\n- All tests passed.",
|
||||
"summary": "# CI Health Update\n- Fixed bug in `src/lock-ops.mjs`: `getRelayFallbacks` is async but was called synchronously in `LockPublisher` constructor.\n- Fixed bug in `src/lock-ops.mjs`: `LockPublisher` used `maybeLogHealthSnapshot` which was undefined; updated to use `healthManager`.\n-…",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"daily",
|
||||
"store",
|
||||
"insight"
|
||||
],
|
||||
"importance": 0.8,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771357144123,
|
||||
"last_seen": 1771357144123,
|
||||
"source": "agent-output",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"a9bc3899-1685-4e50-80f5-ae4dc54de6bd",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "a9bc3899-1685-4e50-80f5-ae4dc54de6bd",
|
||||
"agent_id": "scheduler-memory-weekly",
|
||||
"session_id": "session-mlr4ba92",
|
||||
"type": "batch_window",
|
||||
"content": "# Learnings - 2026-02-17\n\n- The `content-audit-agent` is configured to audit the `/content` directory, but this directory does not exist in the repository root.\n- Added `content-audit-agent targets missing /content directory` to `KNOWN_ISSUES.md`.\n- No audit actions were performed.",
|
||||
"summary": "# Learnings - 2026-02-17\n\n- The `content-audit-agent` is configured to audit the `/content` directory, but this directory does not exist in the repository root.\n- Added `content-audit-agent targets missing /content directory` to `KNOWN_ISSUES.md`.\n- No audit actions were perform…",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"store",
|
||||
"insight"
|
||||
],
|
||||
"importance": 0.8,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771363870888,
|
||||
"last_seen": 1771363870888,
|
||||
"source": "agent-output",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"8512327a-8ba7-4a49-9275-41860e38a2e0",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "8512327a-8ba7-4a49-9275-41860e38a2e0",
|
||||
"agent_id": "scheduler-memory-daily",
|
||||
"session_id": "session-mlr7wt48",
|
||||
"type": "batch_window",
|
||||
"content": "Memory retrieval seed for daily :: scheduler memory retrieval",
|
||||
"summary": "Memory retrieval seed for daily :: scheduler memory retrieval",
|
||||
@@ -41,8 +93,8 @@
|
||||
],
|
||||
"importance": 0.4,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771295958645,
|
||||
"last_seen": 1771295958659,
|
||||
"created_at": 1771369913961,
|
||||
"last_seen": 1771377118848,
|
||||
"source": "scheduler-retrieve",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
@@ -50,24 +102,93 @@
|
||||
}
|
||||
],
|
||||
[
|
||||
"2e9129bd-c366-4d82-ac2b-7e50574532a9",
|
||||
"72f2cb07-62ab-4460-a994-4da196860e6c",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "2e9129bd-c366-4d82-ac2b-7e50574532a9",
|
||||
"agent_id": "onboarding-audit-agent",
|
||||
"session_id": "initial-seed",
|
||||
"id": "72f2cb07-62ab-4460-a994-4da196860e6c",
|
||||
"agent_id": "agent-telemetry",
|
||||
"session_id": "s-telemetry",
|
||||
"type": "batch_window",
|
||||
"content": "Store memory event A for daily :: Required startup + artifacts + memory + issue capture\nStore memory event B for daily :: Required startup + artifacts + memory + issue capture",
|
||||
"summary": "Store memory event A for daily :: Required startup + artifacts + memory + issue capture Store memory event B for daily :: Required startup + artifacts + memory + issue capture",
|
||||
"content": "customer token ABC123 secret",
|
||||
"summary": "customer token ABC123 secret",
|
||||
"tags": [
|
||||
"incident"
|
||||
],
|
||||
"importance": 0.25,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771370050385,
|
||||
"last_seen": 1771385274968,
|
||||
"source": "ingest",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"ed46b70f-334d-4b41-984b-56e0415ffda2",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "ed46b70f-334d-4b41-984b-56e0415ffda2",
|
||||
"agent_id": "agent-telemetry",
|
||||
"session_id": "s-telemetry",
|
||||
"type": "batch_window",
|
||||
"content": "customer token ABC123 secret",
|
||||
"summary": "customer token ABC123 secret",
|
||||
"tags": [
|
||||
"incident"
|
||||
],
|
||||
"importance": 0.25,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771370084740,
|
||||
"last_seen": 1771385274968,
|
||||
"source": "ingest",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"160c683e-c1b0-4b57-8b2a-58e01ecec0e2",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "160c683e-c1b0-4b57-8b2a-58e01ecec0e2",
|
||||
"agent_id": "agent-telemetry",
|
||||
"session_id": "s-telemetry",
|
||||
"type": "batch_window",
|
||||
"content": "customer token ABC123 secret",
|
||||
"summary": "customer token ABC123 secret",
|
||||
"tags": [
|
||||
"incident"
|
||||
],
|
||||
"importance": 0.25,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771370148079,
|
||||
"last_seen": 1771385274968,
|
||||
"source": "ingest",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"38bb52c3-8262-4469-a15e-b46e11e92f9d",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "38bb52c3-8262-4469-a15e-b46e11e92f9d",
|
||||
"agent_id": "scheduler-memory-weekly",
|
||||
"session_id": "session-mlr82pk9",
|
||||
"type": "batch_window",
|
||||
"content": "Store memory event A for weekly :: scheduler memory store\nStore memory event B for weekly :: scheduler memory store",
|
||||
"summary": "Store memory event A for weekly :: scheduler memory store Store memory event B for weekly :: scheduler memory store",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"daily",
|
||||
"weekly",
|
||||
"store"
|
||||
],
|
||||
"importance": 0.55,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771296574057,
|
||||
"last_seen": 1771296574172,
|
||||
"created_at": 1771370189289,
|
||||
"last_seen": 1771370189290,
|
||||
"source": "scheduler-store",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
@@ -75,15 +196,15 @@
|
||||
}
|
||||
],
|
||||
[
|
||||
"2612b9ce-1aa7-4731-acb2-c3a852eeaac6",
|
||||
"f8711702-8dc2-474b-9142-0798f41209f3",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "2612b9ce-1aa7-4731-acb2-c3a852eeaac6",
|
||||
"agent_id": "onboarding-audit-agent",
|
||||
"session_id": "initial-seed",
|
||||
"id": "f8711702-8dc2-474b-9142-0798f41209f3",
|
||||
"agent_id": "scheduler-memory-daily",
|
||||
"session_id": "session-mlrc78ft",
|
||||
"type": "batch_window",
|
||||
"content": "Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture",
|
||||
"summary": "Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture",
|
||||
"content": "Memory retrieval seed for daily :: scheduler memory retrieval",
|
||||
"summary": "Memory retrieval seed for daily :: scheduler memory retrieval",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"daily",
|
||||
@@ -91,8 +212,8 @@
|
||||
],
|
||||
"importance": 0.4,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771296574158,
|
||||
"last_seen": 1771296574172,
|
||||
"created_at": 1771377118841,
|
||||
"last_seen": 1771377118848,
|
||||
"source": "scheduler-retrieve",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
@@ -100,10 +221,35 @@
|
||||
}
|
||||
],
|
||||
[
|
||||
"facf005a-dad0-4066-921c-49a8e408665c",
|
||||
"782a835a-88d1-4307-8154-599a09bcdfb6",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "facf005a-dad0-4066-921c-49a8e408665c",
|
||||
"id": "782a835a-88d1-4307-8154-599a09bcdfb6",
|
||||
"agent_id": "scheduler-memory-weekly",
|
||||
"session_id": "session-mlrc8az1",
|
||||
"type": "batch_window",
|
||||
"content": "Store memory event A for weekly :: scheduler memory store\nStore memory event B for weekly :: scheduler memory store",
|
||||
"summary": "Store memory event A for weekly :: scheduler memory store Store memory event B for weekly :: scheduler memory store",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"store"
|
||||
],
|
||||
"importance": 0.55,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771377168781,
|
||||
"last_seen": 1771377168782,
|
||||
"source": "scheduler-store",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"0c0f8d8f-b1c5-4b4f-bd15-4a53462f3a31",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "0c0f8d8f-b1c5-4b4f-bd15-4a53462f3a31",
|
||||
"agent_id": "agent-telemetry",
|
||||
"session_id": "s-telemetry",
|
||||
"type": "batch_window",
|
||||
@@ -114,8 +260,8 @@
|
||||
],
|
||||
"importance": 0.25,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771296636016,
|
||||
"last_seen": 1771301051605,
|
||||
"created_at": 1771385166452,
|
||||
"last_seen": 1771385274968,
|
||||
"source": "ingest",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
@@ -123,35 +269,10 @@
|
||||
}
|
||||
],
|
||||
[
|
||||
"19ef312e-a725-4544-850e-651713e2d9dc",
|
||||
"5c3af7fa-cab8-4bcf-b1dc-fc845d611026",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "19ef312e-a725-4544-850e-651713e2d9dc",
|
||||
"agent_id": "bug-reproducer-agent",
|
||||
"session_id": "session-mlq2vq5p",
|
||||
"type": "batch_window",
|
||||
"content": "Memory retrieval seed for weekly :: Required startup + artifacts + memory + issue capture",
|
||||
"summary": "Memory retrieval seed for weekly :: Required startup + artifacts + memory + issue capture",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"retrieve"
|
||||
],
|
||||
"importance": 0.4,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771300999213,
|
||||
"last_seen": 1771300999222,
|
||||
"source": "scheduler-retrieve",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"bfbb3c6e-095d-4ccd-ad5a-140b6927421c",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "bfbb3c6e-095d-4ccd-ad5a-140b6927421c",
|
||||
"id": "5c3af7fa-cab8-4bcf-b1dc-fc845d611026",
|
||||
"agent_id": "agent-telemetry",
|
||||
"session_id": "s-telemetry",
|
||||
"type": "batch_window",
|
||||
@@ -162,8 +283,8 @@
|
||||
],
|
||||
"importance": 0.25,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771301051598,
|
||||
"last_seen": 1771301051605,
|
||||
"created_at": 1771385220726,
|
||||
"last_seen": 1771385274968,
|
||||
"source": "ingest",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
@@ -171,50 +292,22 @@
|
||||
}
|
||||
],
|
||||
[
|
||||
"d74e3e34-6cf1-465e-b4a8-64acd0a8d803",
|
||||
"6a7b3ba4-d713-4f18-a98e-d16c0e0837eb",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "d74e3e34-6cf1-465e-b4a8-64acd0a8d803",
|
||||
"agent_id": "bug-reproducer-agent",
|
||||
"session_id": "session-mlq2ysd2",
|
||||
"id": "6a7b3ba4-d713-4f18-a98e-d16c0e0837eb",
|
||||
"agent_id": "todo-triage-agent",
|
||||
"session_id": "2026-02-17T16-35-41Z",
|
||||
"type": "batch_window",
|
||||
"content": "Reproduced npm test timeout issue. Created reproduction script in artifacts/reproducers/npm-test-timeout/. Confirmed that `npm test` hangs/times out as expected.",
|
||||
"summary": "Reproduced npm test timeout issue. Created reproduction script in artifacts/reproducers/npm-test-timeout/. Confirmed that `npm test` hangs/times out as expected.",
|
||||
"content": "customer token ABC123 secret",
|
||||
"summary": "customer token ABC123 secret",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"store",
|
||||
"insight"
|
||||
"incident"
|
||||
],
|
||||
"importance": 0.8,
|
||||
"importance": 0.25,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771301142040,
|
||||
"last_seen": 1771301142040,
|
||||
"source": "agent-output",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"3df7bccf-f3f1-45b9-ab15-88a1a30c6453",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "3df7bccf-f3f1-45b9-ab15-88a1a30c6453",
|
||||
"agent_id": "changelog-agent",
|
||||
"session_id": "session-mlq51t0r",
|
||||
"type": "batch_window",
|
||||
"content": "Memory retrieval seed for weekly :: Required startup + artifacts + memory + issue capture",
|
||||
"summary": "Memory retrieval seed for weekly :: Required startup + artifacts + memory + issue capture",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"retrieve"
|
||||
],
|
||||
"importance": 0.4,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771304642091,
|
||||
"last_seen": 1771304642101,
|
||||
"created_at": 1771346141249,
|
||||
"last_seen": 1771346141259,
|
||||
"source": "scheduler-retrieve",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
@@ -222,178 +315,22 @@
|
||||
}
|
||||
],
|
||||
[
|
||||
"5b32be34-78f9-44ac-b7b1-e301e34f04d0",
|
||||
"ae3f6a26-c753-4a96-b56f-c737a847cee5",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "5b32be34-78f9-44ac-b7b1-e301e34f04d0",
|
||||
"agent_id": "changelog-agent",
|
||||
"session_id": "session-mlq53the",
|
||||
"id": "ae3f6a26-c753-4a96-b56f-c737a847cee5",
|
||||
"agent_id": "todo-triage-agent",
|
||||
"session_id": "2026-02-17T16-36-25Z",
|
||||
"type": "batch_window",
|
||||
"content": "# Changelog Update - 2026-02-17\n\nGenerated draft release notes covering 212 commits.\nKey areas:\n- Scheduler automation improvements\n- Memory management features\n- Codebase modularization",
|
||||
"summary": "# Changelog Update - 2026-02-17\n\nGenerated draft release notes covering 212 commits.\nKey areas:\n- Scheduler automation improvements\n- Memory management features\n- Codebase modularization",
|
||||
"content": "# 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.",
|
||||
"summary": "# 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.",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"store",
|
||||
"insight"
|
||||
"incident"
|
||||
],
|
||||
"importance": 0.8,
|
||||
"importance": 0.25,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771304736005,
|
||||
"last_seen": 1771304736005,
|
||||
"source": "agent-output",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"40f6bf1a-9715-4906-82d1-8f68be78eb3f",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "40f6bf1a-9715-4906-82d1-8f68be78eb3f",
|
||||
"agent_id": "dead-code-agent",
|
||||
"session_id": "1771338346",
|
||||
"type": "batch_window",
|
||||
"content": "Memory retrieval seed for weekly :: Required startup + artifacts + memory + issue capture",
|
||||
"summary": "Memory retrieval seed for weekly :: Required startup + artifacts + memory + issue capture",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"retrieve"
|
||||
],
|
||||
"importance": 0.4,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771338346815,
|
||||
"last_seen": 1771338346833,
|
||||
"source": "scheduler-retrieve",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"267412f9-9704-4739-8481-8317a89e6769",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "267412f9-9704-4739-8481-8317a89e6769",
|
||||
"agent_id": "dead-code-agent",
|
||||
"session_id": "1771338501",
|
||||
"type": "batch_window",
|
||||
"content": "# Weekly Dead Code Analysis - 2026-02-17\n\n**Agent:** dead-code-agent\n**Cadence:** weekly\n**Status:** Completed\n\n## Summary\nExecuted a safety-first dead-code sweep.\n\n## Findings\n- Identified `scripts/fix-duplication.mjs` as an unused standalone script.\n- Identified `src/services/memory/db-adapter.js` as an unused module (exports `createMemoryRepository` but not imported anywhere).\n\n## Actions\n- Verified zero usage for both files using `grep`.\n- Deleted both files in branch `ai/dead-code-20260217`.\n- Verified deletions with `ls` and ran `npm run lint` and `npm run test:unit:lock-backend`.\n- Artifacts generated in `artifacts/dead-code-20260217/`.",
|
||||
"summary": "# Weekly Dead Code Analysis - 2026-02-17\n\n**Agent:** dead-code-agent\n**Cadence:** weekly\n**Status:** Completed\n\n## Summary\nExecuted a safety-first dead-code sweep.\n\n## Findings\n- Identified `scripts/fix-duplication.mjs` as an unused standalone script.\n- Identified `src/services/…",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"store",
|
||||
"insight"
|
||||
],
|
||||
"importance": 0.8,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771338501891,
|
||||
"last_seen": 1771338501891,
|
||||
"source": "agent-output",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"fd852345-c9c5-4c5c-8076-7e16a7b90016",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "fd852345-c9c5-4c5c-8076-7e16a7b90016",
|
||||
"agent_id": "feature-proposer-agent",
|
||||
"session_id": "session-1771346034",
|
||||
"type": "batch_window",
|
||||
"content": "Memory retrieval seed for weekly :: Required startup + artifacts + memory + issue capture",
|
||||
"summary": "Memory retrieval seed for weekly :: Required startup + artifacts + memory + issue capture",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"retrieve"
|
||||
],
|
||||
"importance": 0.4,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771346034674,
|
||||
"last_seen": 1771346034684,
|
||||
"source": "scheduler-retrieve",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"abe5bddd-5b7a-4e18-aabf-9d517bd59a8e",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "abe5bddd-5b7a-4e18-aabf-9d517bd59a8e",
|
||||
"agent_id": "feature-proposer-agent",
|
||||
"session_id": "session-1771346112",
|
||||
"type": "batch_window",
|
||||
"content": "# Memory Update - Feature Proposer Agent\n\n**Date**: 2026-02-17T15-00-00Z\n**Agent**: feature-proposer-agent\n\n## Insights\n- The `task-logs` structure is consistent and easy to parse.\n- A simple CLI tool (`features/log-summary.mjs`) was added to visualize this data.\n- This pattern of \"small, standalone utility scripts\" in `features/` is effective for incremental improvement.\n\n## Actions\n- Implemented `features/log-summary.mjs`.\n- Verified with manual execution.\n\n## Recommendations\n- Future agents could enhance this script to filter by status (e.g., show only failed runs).",
|
||||
"summary": "# Memory Update - Feature Proposer Agent\n\n**Date**: 2026-02-17T15-00-00Z\n**Agent**: feature-proposer-agent\n\n## Insights\n- The `task-logs` structure is consistent and easy to parse.\n- A simple CLI tool (`features/log-summary.mjs`) was added to visualize this data.\n- This pattern …",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"weekly",
|
||||
"store",
|
||||
"insight"
|
||||
],
|
||||
"importance": 0.8,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771346112755,
|
||||
"last_seen": 1771346112755,
|
||||
"source": "agent-output",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"9f17b770-803f-401b-8bec-4c8ce413f232",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "9f17b770-803f-401b-8bec-4c8ce413f232",
|
||||
"agent_id": "audit-agent",
|
||||
"session_id": "session-mlqwxsc8",
|
||||
"type": "batch_window",
|
||||
"content": "Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture",
|
||||
"summary": "Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"daily",
|
||||
"retrieve"
|
||||
],
|
||||
"importance": 0.4,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771351483834,
|
||||
"last_seen": 1771351483843,
|
||||
"source": "scheduler-retrieve",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
"pinned": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"30b53ec3-5d17-4b38-9d80-4f9c15e3cd7e",
|
||||
{
|
||||
"schema_version": 1,
|
||||
"id": "30b53ec3-5d17-4b38-9d80-4f9c15e3cd7e",
|
||||
"agent_id": "audit-agent",
|
||||
"session_id": "session-mlqwzwl4",
|
||||
"type": "batch_window",
|
||||
"content": "# Audit Agent Report (2026-02-17)\n\n## Metrics\n- Oversized files: 18 (4010 excess lines)\n- InnerHTML assignments: 5\n- Lint failures: 13\n\n## Findings\n- Top oversized: scripts/agent/run-scheduler-cycle.mjs (1092 lines)\n- InnerHTML usage in dashboard/index.html and landing/index.html.\n- Lint warnings mostly unused vars.",
|
||||
"summary": "# Audit Agent Report (2026-02-17)\n\n## Metrics\n- Oversized files: 18 (4010 excess lines)\n- InnerHTML assignments: 5\n- Lint failures: 13\n\n## Findings\n- Top oversized: scripts/agent/run-scheduler-cycle.mjs (1092 lines)\n- InnerHTML usage in dashboard/index.html and landing/index.htm…",
|
||||
"tags": [
|
||||
"scheduler",
|
||||
"daily",
|
||||
"store",
|
||||
"insight"
|
||||
],
|
||||
"importance": 0.8,
|
||||
"embedding_id": null,
|
||||
"created_at": 1771351582653,
|
||||
"last_seen": 1771351582653,
|
||||
"created_at": 1771346312937,
|
||||
"last_seen": 1771346312937,
|
||||
"source": "agent-output",
|
||||
"ttl_days": null,
|
||||
"merged_into": null,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"cadence": "daily",
|
||||
"operation": "retrieve",
|
||||
"runId": "session-mlr07hyw",
|
||||
"servicePath": "src/services/memory/index.js#getRelevantMemories",
|
||||
"inputs": {
|
||||
"agentId": "ci-health-agent",
|
||||
"promptPath": "src/prompts/daily/ci-health-agent.md",
|
||||
"promptIntent": "Required startup + artifacts + memory + issue capture",
|
||||
"query": "scheduler | daily | agent | ci-health-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,18 @@
|
||||
{
|
||||
"cadence": "daily",
|
||||
"operation": "retrieve",
|
||||
"runId": "session-mlr0921p",
|
||||
"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": 1
|
||||
},
|
||||
"status": "ok"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MEMORY_RETRIEVED
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"cadence": "daily",
|
||||
"operation": "store",
|
||||
"runId": "session-mlr0b3uh",
|
||||
"servicePath": "src/services/memory/index.js#ingestEvents",
|
||||
"inputs": {
|
||||
"agentId": "ci-health-agent",
|
||||
"promptPath": "src/prompts/daily/ci-health-agent.md",
|
||||
"promptIntent": "Required startup + artifacts + memory + issue capture",
|
||||
"events": 1,
|
||||
"sourceFile": "memory-updates/2026-02-17T19-36-32Z__ci-health-agent.md"
|
||||
},
|
||||
"outputs": {
|
||||
"storedCount": 1,
|
||||
"summaries": [
|
||||
"# CI Health Update\n- Fixed bug in `src/lock-ops.mjs`: `getRelayFallbacks` is async but was called synchronously in `LockPublisher` constructor.\n- Fixed bug in `src/lock-ops.mjs`: `LockPublisher` used `maybeLogHealthSnapshot` which was undefined; updated to use `healthManager`.\n-…"
|
||||
]
|
||||
},
|
||||
"status": "ok"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MEMORY_STORED
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"cadence": "daily",
|
||||
"operation": "retrieve",
|
||||
"runId": "session-mlr0dhq6",
|
||||
"servicePath": "src/services/memory/index.js#getRelevantMemories",
|
||||
"inputs": {
|
||||
"agentId": "const-refactor-agent",
|
||||
"promptPath": "src/prompts/daily/const-refactor-agent.md",
|
||||
"promptIntent": "Required startup + artifacts + memory + issue capture",
|
||||
"query": "scheduler | daily | agent | const-refactor-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": "weekly",
|
||||
"operation": "store",
|
||||
"runId": "session-mlr0g2tv",
|
||||
"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
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"cadence": "daily",
|
||||
"operation": "store",
|
||||
"runId": "session-mlr0iplm",
|
||||
"servicePath": "src/services/memory/index.js#ingestEvents",
|
||||
"inputs": {
|
||||
"agentId": "const-refactor-agent",
|
||||
"promptPath": "src/prompts/daily/const-refactor-agent.md",
|
||||
"promptIntent": "Required startup + artifacts + memory + issue capture",
|
||||
"events": 1,
|
||||
"sourceFile": "memory-updates/2026-02-17T19-44-58Z__const-refactor-agent.md"
|
||||
},
|
||||
"outputs": {
|
||||
"storedCount": 1,
|
||||
"summaries": [
|
||||
"# Memory Update: Constants Refactor\n\n- Discovered hardcoded constants in `src/lock-ops.mjs` duplicating defaults.\n- Extracted `DEFAULT_RETRY_ATTEMPTS`, `DEFAULT_RETRY_BASE_DELAY_MS`, etc. to `src/constants.mjs`.\n- Refactored `src/lock-ops.mjs` to use these constants.\n- Fixed a b…"
|
||||
]
|
||||
},
|
||||
"status": "ok"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MEMORY_STORED
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"cadence": "weekly",
|
||||
"operation": "store",
|
||||
"runId": "session-mlr4ba92",
|
||||
"servicePath": "src/services/memory/index.js#ingestEvents",
|
||||
"inputs": {
|
||||
"agentId": "scheduler-memory-weekly",
|
||||
"promptPath": "src/prompts/daily/.md",
|
||||
"promptIntent": "scheduler memory store",
|
||||
"events": 1,
|
||||
"sourceFile": "memory-updates/2026-02-17T21-29-25Z__content-audit-agent.md"
|
||||
},
|
||||
"outputs": {
|
||||
"storedCount": 1,
|
||||
"summaries": [
|
||||
"# Learnings - 2026-02-17\n\n- The `content-audit-agent` is configured to audit the `/content` directory, but this directory does not exist in the repository root.\n- Added `content-audit-agent targets missing /content directory` to `KNOWN_ISSUES.md`.\n- No audit actions were perform…"
|
||||
]
|
||||
},
|
||||
"status": "ok"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MEMORY_STORED
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"cadence": "daily",
|
||||
"operation": "retrieve",
|
||||
"runId": "session-mlr7wt48",
|
||||
"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": 1
|
||||
},
|
||||
"status": "ok"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MEMORY_RETRIEVED
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"cadence": "weekly",
|
||||
"operation": "store",
|
||||
"runId": "session-mlr82pk9",
|
||||
"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
|
||||
@@ -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
@@ -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.
|
||||
@@ -65,3 +65,11 @@ Track only **active, reproducible, unresolved** issues.
|
||||
- **Related notes:** `docs/agent-handoffs/learnings/2026-02-15-relay-health-preflight-job.md`
|
||||
- **Last verified:** 2026-02-15
|
||||
|
||||
### content-audit-agent targets missing /content directory
|
||||
- **Status:** Active
|
||||
- **Area:** Docs
|
||||
- **Symptom:** `content-audit-agent` fails to find `/content` directory.
|
||||
- **Trigger/Conditions:** Running `content-audit-agent`.
|
||||
- **Workaround:** None. Requires updating the agent prompt or repo structure.
|
||||
- **Impact:** Docs audit cannot be performed.
|
||||
- **Last verified:** 2026-02-17
|
||||
@@ -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
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
# Dependency Report (2026-02-14)
|
||||
# Dependency Security Report
|
||||
|
||||
## Summary
|
||||
- Vulnerabilities: 0
|
||||
- Outdated Packages: 1
|
||||
**Date:** 2026-02-17
|
||||
|
||||
## Vulnerabilities
|
||||
No vulnerabilities found.
|
||||
Found 4 moderate vulnerabilities in `eslint` stack.
|
||||
- `eslint`, `@eslint/js`, `ajv`, `@eslint-community/eslint-utils`.
|
||||
- **Note**: Current installed version is `^10.0.0`, which exceeds normal registry versions. Investigation needed.
|
||||
|
||||
## Outdated Packages
|
||||
- **nostr-tools**: Current `2.19.4` -> Latest `2.23.1` (Wanted: `2.19.4`)
|
||||
- **nostr-tools**:
|
||||
- Current: 2.19.4
|
||||
- Latest: 2.23.1
|
||||
- **Action**: Skipped auto-upgrade (Protocol Library). Please review manually.
|
||||
|
||||
## Recommendations
|
||||
- **nostr-tools**: This is a protocol library. Auto-upgrade is disabled. Recommend manual review for upgrade to 2.23.1.
|
||||
## Actions Taken
|
||||
- No automatic upgrades performed due to safety constraints.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Inventory - 2026-02-17
|
||||
|
||||
## Scope
|
||||
Audit of `/content` directory for upload/contribution docs.
|
||||
|
||||
## Findings
|
||||
- **Missing Directory**: The `/content` directory does not exist in the repository root.
|
||||
- **Impact**: Unable to verify claims about upload functionality or contribution guidelines in `/content`.
|
||||
- **Status**: Failed to inventory due to missing target.
|
||||
|
||||
## Recommendations
|
||||
- Verify if `/content` is the intended target or if documentation resides elsewhere (e.g., `src/docs`).
|
||||
- Update `content-audit-agent` prompt to reflect the correct directory structure.
|
||||
+102
-6
@@ -1,22 +1,118 @@
|
||||
{
|
||||
"auditReportVersion": 2,
|
||||
"vulnerabilities": {},
|
||||
"vulnerabilities": {
|
||||
"@eslint-community/eslint-utils": {
|
||||
"name": "@eslint-community/eslint-utils",
|
||||
"severity": "moderate",
|
||||
"isDirect": false,
|
||||
"via": [
|
||||
"eslint"
|
||||
],
|
||||
"effects": [
|
||||
"eslint"
|
||||
],
|
||||
"range": "*",
|
||||
"nodes": [
|
||||
"node_modules/@eslint-community/eslint-utils"
|
||||
],
|
||||
"fixAvailable": {
|
||||
"name": "eslint",
|
||||
"version": "4.1.1",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"@eslint/js": {
|
||||
"name": "@eslint/js",
|
||||
"severity": "moderate",
|
||||
"isDirect": true,
|
||||
"via": [
|
||||
"eslint"
|
||||
],
|
||||
"effects": [],
|
||||
"range": ">=10.0.1",
|
||||
"nodes": [
|
||||
"node_modules/@eslint/js"
|
||||
],
|
||||
"fixAvailable": {
|
||||
"name": "@eslint/js",
|
||||
"version": "9.39.2",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"name": "ajv",
|
||||
"severity": "moderate",
|
||||
"isDirect": false,
|
||||
"via": [
|
||||
{
|
||||
"source": 1113214,
|
||||
"name": "ajv",
|
||||
"dependency": "ajv",
|
||||
"title": "ajv has ReDoS when using `$data` option",
|
||||
"url": "https://github.com/advisories/GHSA-2g4f-4pwh-qvx6",
|
||||
"severity": "moderate",
|
||||
"cwe": [
|
||||
"CWE-400"
|
||||
],
|
||||
"cvss": {
|
||||
"score": 0,
|
||||
"vectorString": null
|
||||
},
|
||||
"range": "<8.18.0"
|
||||
}
|
||||
],
|
||||
"effects": [
|
||||
"eslint"
|
||||
],
|
||||
"range": "<8.18.0",
|
||||
"nodes": [
|
||||
"node_modules/ajv"
|
||||
],
|
||||
"fixAvailable": {
|
||||
"name": "eslint",
|
||||
"version": "4.1.1",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
},
|
||||
"eslint": {
|
||||
"name": "eslint",
|
||||
"severity": "moderate",
|
||||
"isDirect": true,
|
||||
"via": [
|
||||
"@eslint-community/eslint-utils",
|
||||
"ajv"
|
||||
],
|
||||
"effects": [
|
||||
"@eslint-community/eslint-utils",
|
||||
"@eslint/js"
|
||||
],
|
||||
"range": ">=4.2.0",
|
||||
"nodes": [
|
||||
"node_modules/eslint"
|
||||
],
|
||||
"fixAvailable": {
|
||||
"name": "eslint",
|
||||
"version": "4.1.1",
|
||||
"isSemVerMajor": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"moderate": 4,
|
||||
"high": 0,
|
||||
"critical": 0,
|
||||
"total": 0
|
||||
"total": 4
|
||||
},
|
||||
"dependencies": {
|
||||
"prod": 12,
|
||||
"dev": 72,
|
||||
"optional": 0,
|
||||
"dev": 76,
|
||||
"optional": 1,
|
||||
"peer": 0,
|
||||
"peerOptional": 0,
|
||||
"total": 83
|
||||
"total": 87
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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');
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
> [email protected] dashboard:serve
|
||||
> node bin/torch-lock.mjs dashboard
|
||||
|
||||
Dashboard running at http://null:4173/dashboard/
|
||||
Authentication: DISABLED (Dashboard is public)
|
||||
Serving files from /app
|
||||
Using configuration from /app
|
||||
@@ -2,6 +2,7 @@
|
||||
> [email protected] dashboard:serve
|
||||
> node bin/torch-lock.mjs dashboard
|
||||
|
||||
Dashboard running at http://localhost:4173/dashboard/
|
||||
Dashboard running at http://null:4173/dashboard/
|
||||
Authentication: DISABLED (Dashboard is public)
|
||||
Serving files from /app
|
||||
Using configuration from /app
|
||||
+6
-6
@@ -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.
|
||||
@@ -0,0 +1,6 @@
|
||||
# CI Health Update
|
||||
- Fixed bug in `src/lock-ops.mjs`: `getRelayFallbacks` is async but was called synchronously in `LockPublisher` constructor.
|
||||
- Fixed bug in `src/lock-ops.mjs`: `LockPublisher` used `maybeLogHealthSnapshot` which was undefined; updated to use `healthManager`.
|
||||
- Fixed bug in `src/cmd-check.mjs`: `queryLocksFn` was undefined; updated to use injected `queryLocksFn` or default `queryLocks`.
|
||||
- Ran `npm run build` to fix `test/build_verification.test.mjs`.
|
||||
- All tests passed.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Memory Update: Constants Refactor
|
||||
|
||||
- Discovered hardcoded constants in `src/lock-ops.mjs` duplicating defaults.
|
||||
- Extracted `DEFAULT_RETRY_ATTEMPTS`, `DEFAULT_RETRY_BASE_DELAY_MS`, etc. to `src/constants.mjs`.
|
||||
- Refactored `src/lock-ops.mjs` to use these constants.
|
||||
- Fixed a bug in `src/cmd-check.mjs` where `queryLocksFn` was undefined.
|
||||
- Verified with unit tests and lint.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Learnings - 2026-02-17
|
||||
|
||||
- The `content-audit-agent` is configured to audit the `/content` directory, but this directory does not exist in the repository root.
|
||||
- Added `content-audit-agent targets missing /content directory` to `KNOWN_ISSUES.md`.
|
||||
- No audit actions were performed.
|
||||
Generated
+64
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"eslint": "^10.0.0",
|
||||
"globals": "^17.3.0"
|
||||
}
|
||||
@@ -255,6 +256,22 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||
@@ -670,6 +687,21 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -952,6 +984,38 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"eslint": "^10.0.0",
|
||||
"globals": "^17.3.0"
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
@@ -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');
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,19 @@
|
||||
TORCH.md: 342 lines (excess: 42)
|
||||
dashboard/index.html: 1044 lines (excess: 744)
|
||||
dashboard/styles.css: 361 lines (excess: 61)
|
||||
landing/index.html: 924 lines (excess: 624)
|
||||
perf/constants-refactor/candidates.json: 1668 lines (excess: 1368)
|
||||
scripts/agent/run-scheduler-cycle.mjs: 1127 lines (excess: 827)
|
||||
scripts/agent/verify-run-artifacts.mjs: 375 lines (excess: 75)
|
||||
src/lib.mjs: 685 lines (excess: 385)
|
||||
src/lock-ops.mjs: 702 lines (excess: 402)
|
||||
src/ops.mjs: 450 lines (excess: 150)
|
||||
src/services/memory/index.js: 440 lines (excess: 140)
|
||||
src/services/memory/pruner.js: 314 lines (excess: 14)
|
||||
src/services/memory/scheduler.js: 318 lines (excess: 18)
|
||||
src/torch-config.mjs: 312 lines (excess: 12)
|
||||
test/lib.test.mjs: 418 lines (excess: 118)
|
||||
test/lock-ops.test.mjs: 395 lines (excess: 95)
|
||||
test/memory-formatter.test.mjs: 442 lines (excess: 142)
|
||||
test/run-scheduler-cycle-memory-policy.test.mjs: 599 lines (excess: 299)
|
||||
test-audit/flakiness-matrix.json: 366 lines (excess: 66)
|
||||
@@ -0,0 +1,49 @@
|
||||
import { chromium } from '@playwright/test';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
let errorCount = 0;
|
||||
|
||||
page.on('console', msg => {
|
||||
const type = msg.type();
|
||||
if (type === 'error' || type === 'warning') {
|
||||
console.log(`CONSOLE [${type.toUpperCase()}]: ${msg.text()}`);
|
||||
if (type === 'error') errorCount++;
|
||||
}
|
||||
});
|
||||
|
||||
page.on('pageerror', exception => {
|
||||
console.log(`PAGE ERROR: ${exception}`);
|
||||
errorCount++;
|
||||
});
|
||||
|
||||
page.on('requestfailed', request => {
|
||||
// Check if failure() is not null before accessing errorText
|
||||
const failure = request.failure();
|
||||
const errorText = failure ? failure.errorText : 'Unknown error';
|
||||
console.log(`REQUEST FAILED: ${request.url()} - ${errorText}`);
|
||||
errorCount++;
|
||||
});
|
||||
|
||||
try {
|
||||
console.log('Navigating to http://localhost:4173/dashboard/ ...');
|
||||
await page.goto('http://localhost:4173/dashboard/');
|
||||
console.log('Page loaded. Waiting 5s for any async errors...');
|
||||
await page.waitForTimeout(5000);
|
||||
} catch (e) {
|
||||
console.error('Navigation failed:', e);
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
if (errorCount > 0) {
|
||||
console.log(`\nFound ${errorCount} errors.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\nNo errors found.');
|
||||
process.exit(0);
|
||||
}
|
||||
})();
|
||||
@@ -3,9 +3,28 @@
|
||||
// implemented by this script; step 3 (policy-file read) is best-effort and non-fatal.
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import {
|
||||
runCommand,
|
||||
parseJsonFromOutput,
|
||||
readJson,
|
||||
normalizeStringList,
|
||||
parseNonNegativeInt,
|
||||
parseBooleanFlag,
|
||||
excerptText,
|
||||
getRunDateKey,
|
||||
toYamlScalar,
|
||||
} from './scheduler-utils.mjs';
|
||||
|
||||
import {
|
||||
classifyLockBackendError,
|
||||
buildLockBackendRemediation,
|
||||
runLockHealthPreflight,
|
||||
acquireLockWithRetry,
|
||||
summarizeLockFailureReasons,
|
||||
} from './scheduler-lock.mjs';
|
||||
|
||||
const VALID_CADENCES = new Set(['daily', 'weekly']);
|
||||
const ALL_EXCLUDED_REASON = 'All roster tasks currently claimed by other agents';
|
||||
const FAILURE_CATEGORY = {
|
||||
@@ -15,8 +34,6 @@ const FAILURE_CATEGORY = {
|
||||
EXECUTION: 'execution_error',
|
||||
};
|
||||
|
||||
const LOCK_INCIDENT_LINK = 'docs/agent-handoffs/learnings/2026-02-15-relay-health-preflight-job.md';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { cadence: null, platform: process.env.AGENT_PLATFORM || 'codex', model: process.env.AGENT_MODEL || null };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
@@ -46,145 +63,6 @@ function ts() {
|
||||
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/:/g, '-');
|
||||
}
|
||||
|
||||
function parseJsonFromOutput(text) {
|
||||
const trimmed = String(text || '').trim();
|
||||
if (!trimmed) return null;
|
||||
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||||
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
||||
const line = lines[i];
|
||||
if (!line.startsWith('{') && !line.startsWith('[')) continue;
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
// Keep scanning from the end.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseJsonEventsFromOutput(text) {
|
||||
const events = [];
|
||||
for (const line of String(text || '').split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('{')) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
events.push(parsed);
|
||||
}
|
||||
} catch {
|
||||
// Ignore non-JSON lines.
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
async function runCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, ...(options.env || {}) },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
const OUTPUT_LIMIT = 20000; // 20KB
|
||||
let stdoutWritten = 0;
|
||||
let stderrWritten = 0;
|
||||
let stdoutTruncated = false;
|
||||
let stderrTruncated = false;
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stdout += text;
|
||||
|
||||
if (stdoutWritten < OUTPUT_LIMIT) {
|
||||
const remaining = OUTPUT_LIMIT - stdoutWritten;
|
||||
if (text.length <= remaining) {
|
||||
process.stdout.write(text);
|
||||
stdoutWritten += text.length;
|
||||
} else {
|
||||
process.stdout.write(text.slice(0, remaining));
|
||||
stdoutWritten += remaining;
|
||||
}
|
||||
} else if (!stdoutTruncated) {
|
||||
process.stdout.write('\n...[stdout truncated]...\n');
|
||||
stdoutTruncated = true;
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
|
||||
if (stderrWritten < OUTPUT_LIMIT) {
|
||||
const remaining = OUTPUT_LIMIT - stderrWritten;
|
||||
if (text.length <= remaining) {
|
||||
process.stderr.write(text);
|
||||
stderrWritten += text.length;
|
||||
} else {
|
||||
process.stderr.write(text.slice(0, remaining));
|
||||
stderrWritten += remaining;
|
||||
}
|
||||
} else if (!stderrTruncated) {
|
||||
process.stderr.write('\n...[stderr truncated]...\n');
|
||||
stderrTruncated = true;
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({ code: code ?? 1, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function readJson(filePath, fallback) {
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStringList(value, fallback = []) {
|
||||
if (!Array.isArray(value)) return fallback;
|
||||
return value.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim());
|
||||
}
|
||||
|
||||
function parseNonNegativeInt(value, fallback) {
|
||||
const parsed = Number.parseInt(String(value ?? ''), 10);
|
||||
if (Number.isFinite(parsed) && parsed >= 0) {
|
||||
return parsed;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function parseBooleanFlag(value, fallback = false) {
|
||||
if (value === undefined || value === null || value === '') return fallback;
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
||||
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function redactSensitive(text) {
|
||||
if (!text) return '';
|
||||
return String(text)
|
||||
.replace(/\b(BEARER\s+)[A-Za-z0-9._~+/=-]+/gi, '$1[REDACTED]')
|
||||
.replace(/\b(token|api[_-]?key|secret(?:[_-]?key)?|password|passwd|authorization)\s*[:=]\s*[^\s,;]+/gi, '$1=[REDACTED]')
|
||||
.replace(/\b(sk|pk|ghp|xoxb|xoxp)_[A-Za-z0-9_-]+\b/g, '[REDACTED]');
|
||||
}
|
||||
|
||||
function excerptText(text, maxChars = 600) {
|
||||
const clean = redactSensitive(String(text || '').trim());
|
||||
if (!clean) return '';
|
||||
if (clean.length <= maxChars) return clean;
|
||||
return `${clean.slice(0, maxChars)}…`;
|
||||
}
|
||||
|
||||
function categorizeFailureMetadata(failureCategory, extraMetadata = {}) {
|
||||
return {
|
||||
failure_category: failureCategory,
|
||||
@@ -192,26 +70,6 @@ function categorizeFailureMetadata(failureCategory, extraMetadata = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function formatDurationMs(ms) {
|
||||
if (!Number.isFinite(ms) || ms < 0) return 'unknown';
|
||||
const minutes = Math.round(ms / 60000);
|
||||
return `${minutes} minute(s)`;
|
||||
}
|
||||
|
||||
function buildLockBackendRemediation({ cadence, retryWindowMs, maxDeferrals, incidentSignalId = null }) {
|
||||
const steps = [
|
||||
'Likely backend/relay connectivity issue during lock acquisition.',
|
||||
`Retry window: ${formatDurationMs(retryWindowMs)} (max deferrals: ${maxDeferrals}).`,
|
||||
`Retry command: npm run scheduler:${cadence}`,
|
||||
`Run health check: npm run lock:health -- --cadence ${cadence}`,
|
||||
`Review incident runbook: ${LOCK_INCIDENT_LINK}`,
|
||||
];
|
||||
if (incidentSignalId) {
|
||||
steps.push(`Incident signal: ${incidentSignalId}`);
|
||||
}
|
||||
return `Recommended auto-remediation: ${steps.join(' ')}`;
|
||||
}
|
||||
|
||||
async function validatePromptFile(promptPath) {
|
||||
let content;
|
||||
try {
|
||||
@@ -239,47 +97,6 @@ async function validatePromptFile(promptPath) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function classifyLockBackendError(outputText) {
|
||||
const text = String(outputText || '').toLowerCase();
|
||||
if (!text.trim()) return 'unknown_backend_error';
|
||||
|
||||
const errorCategoryMatch = text.match(/error_category=([a-z0-9_]+)/);
|
||||
if (errorCategoryMatch?.[1]) {
|
||||
return errorCategoryMatch[1];
|
||||
}
|
||||
|
||||
if ((text.includes('relay') || text.includes('query')) && text.includes('timeout')) {
|
||||
return 'relay_query_timeout';
|
||||
}
|
||||
|
||||
if (text.includes('publish failed to all relays') || text.includes('failed to publish to any relay')) {
|
||||
return 'relay_publish_quorum_failure';
|
||||
}
|
||||
|
||||
if (
|
||||
text.includes('connection refused')
|
||||
|| text.includes('econnrefused')
|
||||
|| text.includes('getaddrinfo')
|
||||
|| text.includes('enotfound')
|
||||
|| text.includes('eai_again')
|
||||
|| (text.includes('websocket') && text.includes('dns'))
|
||||
) {
|
||||
return 'websocket_connection_refused_or_dns';
|
||||
}
|
||||
|
||||
if (
|
||||
text.includes('invalid url')
|
||||
|| text.includes('malformed')
|
||||
|| text.includes('unsupported protocol')
|
||||
|| text.includes('must start with ws')
|
||||
|| text.includes('invalid relay')
|
||||
) {
|
||||
return 'malformed_relay_url_config';
|
||||
}
|
||||
|
||||
return 'unknown_backend_error';
|
||||
}
|
||||
|
||||
async function artifactExistsSince(filePath, sinceMs) {
|
||||
if (!filePath) return false;
|
||||
const resolved = path.isAbsolute(filePath)
|
||||
@@ -295,14 +112,8 @@ async function artifactExistsSince(filePath, sinceMs) {
|
||||
|
||||
async function verifyMemoryStep({ name, markers, artifacts, outputText, sinceMs }) {
|
||||
const markerMatched = markers.some((marker) => outputText.includes(marker));
|
||||
let artifactMatched = false;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
if (await artifactExistsSince(artifact, sinceMs)) {
|
||||
artifactMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const artifactResults = await Promise.all(artifacts.map((artifact) => artifactExistsSince(artifact, sinceMs)));
|
||||
const artifactMatched = artifactResults.some(Boolean);
|
||||
|
||||
return {
|
||||
name,
|
||||
@@ -371,22 +182,28 @@ async function getLatestFile(logDir) {
|
||||
.filter((filename) => isStrictSchedulerLogFilename(filename))
|
||||
.sort((a, b) => b.localeCompare(a));
|
||||
|
||||
const results = await Promise.all(
|
||||
files.map(async (filename) => {
|
||||
const filePath = path.join(logDir, filename);
|
||||
const content = await fs.readFile(filePath, 'utf8').catch(() => '');
|
||||
const createdAtMs = parseDateValue(parseFrontmatterCreatedAt(content));
|
||||
const fileTimestampMs = parseTimestampFromFilename(filename);
|
||||
const effectiveMs = createdAtMs ?? fileTimestampMs;
|
||||
|
||||
if (!effectiveMs) {
|
||||
console.warn(`[scheduler] Ignoring invalid log timestamp in ${filename}; checking next candidate.`);
|
||||
return null;
|
||||
}
|
||||
return { filename, effectiveMs };
|
||||
}),
|
||||
);
|
||||
|
||||
let latest = null;
|
||||
|
||||
for (const filename of files) {
|
||||
const filePath = path.join(logDir, filename);
|
||||
const content = await fs.readFile(filePath, 'utf8').catch(() => '');
|
||||
const createdAtMs = parseDateValue(parseFrontmatterCreatedAt(content));
|
||||
const fileTimestampMs = parseTimestampFromFilename(filename);
|
||||
const effectiveMs = createdAtMs ?? fileTimestampMs;
|
||||
|
||||
if (!effectiveMs) {
|
||||
console.warn(`[scheduler] Ignoring invalid log timestamp in ${filename}; checking next candidate.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!latest || effectiveMs > latest.effectiveMs) {
|
||||
latest = { filename, effectiveMs };
|
||||
for (const result of results) {
|
||||
if (!result) continue;
|
||||
if (!latest || result.effectiveMs > latest.effectiveMs) {
|
||||
latest = result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,10 +302,6 @@ async function getSchedulerConfig(cadence, { isInteractive }) {
|
||||
};
|
||||
}
|
||||
|
||||
function getRunDateKey(now = new Date()) {
|
||||
return now.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
async function readRunState(cadence) {
|
||||
const statePath = path.resolve(process.cwd(), 'task-logs', cadence, '.scheduler-run-state.json');
|
||||
const fallback = { run_date: getRunDateKey(), lock_deferral: null };
|
||||
@@ -519,122 +332,6 @@ function createIdempotencyKey({ cadence, selectedAgent, runDate }) {
|
||||
return `${cadence}:${selectedAgent}:${runDate}:${randomUUID()}`;
|
||||
}
|
||||
|
||||
async function runLockHealthPreflight({ cadence, platform }) {
|
||||
const preflight = await runCommand('npm', ['run', 'lock:health', '--', '--cadence', cadence], {
|
||||
env: { AGENT_PLATFORM: platform },
|
||||
});
|
||||
const payload = parseJsonFromOutput(`${preflight.stdout}\n${preflight.stderr}`) || {};
|
||||
const relayList = Array.isArray(payload.relays)
|
||||
? payload.relays.filter((relay) => typeof relay === 'string' && relay.trim()).map((relay) => relay.trim())
|
||||
: [];
|
||||
const combinedOutput = `${preflight.stderr}\n${preflight.stdout}`;
|
||||
const failureCategory = payload.failureCategory || classifyLockBackendError(combinedOutput);
|
||||
|
||||
return {
|
||||
code: preflight.code,
|
||||
payload,
|
||||
relayList,
|
||||
failureCategory,
|
||||
stderrExcerpt: excerptText(preflight.stderr),
|
||||
stdoutExcerpt: excerptText(preflight.stdout),
|
||||
};
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function acquireLockWithRetry({ selectedAgent, cadence, platform, model, lockRetry, idempotencyKey }) {
|
||||
const lockCommandArgs = ['run', 'lock:lock', '--', '--agent', selectedAgent, '--cadence', cadence];
|
||||
if (model) {
|
||||
lockCommandArgs.push('--model', model);
|
||||
}
|
||||
const backoffScheduleMs = [];
|
||||
let attempts = 0;
|
||||
const correlationId = idempotencyKey || `${cadence}:${selectedAgent}:${getRunDateKey()}`;
|
||||
|
||||
while (true) {
|
||||
attempts += 1;
|
||||
const result = await runCommand('npm', lockCommandArgs, {
|
||||
env: {
|
||||
AGENT_PLATFORM: platform,
|
||||
...(model ? { AGENT_MODEL: model } : {}),
|
||||
...(idempotencyKey ? { SCHEDULER_LOCK_IDEMPOTENCY_KEY: idempotencyKey } : {}),
|
||||
SCHEDULER_LOCK_CORRELATION_ID: correlationId,
|
||||
SCHEDULER_LOCK_ATTEMPT_ID: String(attempts),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.code !== 2) {
|
||||
return { result, attempts, backoffScheduleMs, correlationId };
|
||||
}
|
||||
|
||||
if (attempts > lockRetry.maxRetries) {
|
||||
return {
|
||||
result,
|
||||
attempts,
|
||||
backoffScheduleMs,
|
||||
correlationId,
|
||||
finalBackendCategory: classifyLockBackendError(`${result.stderr}\n${result.stdout}`),
|
||||
};
|
||||
}
|
||||
|
||||
const exponentialBase = lockRetry.backoffMs * (2 ** (attempts - 1));
|
||||
const jitter = lockRetry.jitterMs > 0
|
||||
? Math.floor(Math.random() * (lockRetry.jitterMs + 1))
|
||||
: 0;
|
||||
const delayMs = exponentialBase + jitter;
|
||||
backoffScheduleMs.push(delayMs);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
event: 'scheduler.lock.retry',
|
||||
attempt: attempts,
|
||||
max_retries: lockRetry.maxRetries,
|
||||
delay_ms: delayMs,
|
||||
correlation_id: correlationId,
|
||||
selected_agent: selectedAgent,
|
||||
cadence,
|
||||
}));
|
||||
|
||||
if (delayMs > 0) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeLockFailureReasons(outputText) {
|
||||
const events = parseJsonEventsFromOutput(outputText);
|
||||
const quorumFailure = events.findLast((entry) => entry.event === 'lock_publish_quorum_failed');
|
||||
if (quorumFailure && quorumFailure.reasonDistribution && typeof quorumFailure.reasonDistribution === 'object') {
|
||||
return {
|
||||
reasonDistribution: quorumFailure.reasonDistribution,
|
||||
attemptId: quorumFailure.attemptId || null,
|
||||
correlationId: quorumFailure.correlationId || null,
|
||||
totalElapsedMs: quorumFailure.totalElapsedMs ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const distribution = {};
|
||||
for (const entry of events) {
|
||||
if (entry.event !== 'lock_publish_failure') continue;
|
||||
const reason = typeof entry.reason === 'string' && entry.reason.trim()
|
||||
? entry.reason.trim()
|
||||
: 'unknown';
|
||||
distribution[reason] = (distribution[reason] || 0) + 1;
|
||||
}
|
||||
return {
|
||||
reasonDistribution: distribution,
|
||||
attemptId: null,
|
||||
correlationId: null,
|
||||
totalElapsedMs: null,
|
||||
};
|
||||
}
|
||||
|
||||
function toYamlScalar(value) {
|
||||
const str = String(value ?? '');
|
||||
return `'${str.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
async function writeLog({ cadence, agent, status, reason, detail, platform, metadata = {} }) {
|
||||
const logDir = path.resolve(process.cwd(), 'task-logs', cadence);
|
||||
await fs.mkdir(logDir, { recursive: true });
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
runCommand,
|
||||
parseJsonFromOutput,
|
||||
parseJsonEventsFromOutput,
|
||||
excerptText,
|
||||
formatDurationMs,
|
||||
getRunDateKey,
|
||||
sleep,
|
||||
} from './scheduler-utils.mjs';
|
||||
|
||||
const LOCK_INCIDENT_LINK = 'docs/agent-handoffs/learnings/2026-02-15-relay-health-preflight-job.md';
|
||||
|
||||
export function classifyLockBackendError(outputText) {
|
||||
const text = String(outputText || '').toLowerCase();
|
||||
if (!text.trim()) return 'unknown_backend_error';
|
||||
|
||||
const errorCategoryMatch = text.match(/error_category=([a-z0-9_]+)/);
|
||||
if (errorCategoryMatch?.[1]) {
|
||||
return errorCategoryMatch[1];
|
||||
}
|
||||
|
||||
if ((text.includes('relay') || text.includes('query')) && text.includes('timeout')) {
|
||||
return 'relay_query_timeout';
|
||||
}
|
||||
|
||||
if (text.includes('publish failed to all relays') || text.includes('failed to publish to any relay')) {
|
||||
return 'relay_publish_quorum_failure';
|
||||
}
|
||||
|
||||
if (
|
||||
text.includes('connection refused')
|
||||
|| text.includes('econnrefused')
|
||||
|| text.includes('getaddrinfo')
|
||||
|| text.includes('enotfound')
|
||||
|| text.includes('eai_again')
|
||||
|| (text.includes('websocket') && text.includes('dns'))
|
||||
) {
|
||||
return 'websocket_connection_refused_or_dns';
|
||||
}
|
||||
|
||||
if (
|
||||
text.includes('invalid url')
|
||||
|| text.includes('malformed')
|
||||
|| text.includes('unsupported protocol')
|
||||
|| text.includes('must start with ws')
|
||||
|| text.includes('invalid relay')
|
||||
) {
|
||||
return 'malformed_relay_url_config';
|
||||
}
|
||||
|
||||
return 'unknown_backend_error';
|
||||
}
|
||||
|
||||
export function buildLockBackendRemediation({ cadence, retryWindowMs, maxDeferrals, incidentSignalId = null }) {
|
||||
const steps = [
|
||||
'Likely backend/relay connectivity issue during lock acquisition.',
|
||||
`Retry window: ${formatDurationMs(retryWindowMs)} (max deferrals: ${maxDeferrals}).`,
|
||||
`Retry command: npm run scheduler:${cadence}`,
|
||||
`Run health check: npm run lock:health -- --cadence ${cadence}`,
|
||||
`Review incident runbook: ${LOCK_INCIDENT_LINK}`,
|
||||
];
|
||||
if (incidentSignalId) {
|
||||
steps.push(`Incident signal: ${incidentSignalId}`);
|
||||
}
|
||||
return `Recommended auto-remediation: ${steps.join(' ')}`;
|
||||
}
|
||||
|
||||
export async function runLockHealthPreflight({ cadence, platform }) {
|
||||
const preflight = await runCommand('npm', ['run', 'lock:health', '--', '--cadence', cadence], {
|
||||
env: { AGENT_PLATFORM: platform },
|
||||
});
|
||||
const payload = parseJsonFromOutput(`${preflight.stdout}\n${preflight.stderr}`) || {};
|
||||
const relayList = Array.isArray(payload.relays)
|
||||
? payload.relays.filter((relay) => typeof relay === 'string' && relay.trim()).map((relay) => relay.trim())
|
||||
: [];
|
||||
const combinedOutput = `${preflight.stderr}\n${preflight.stdout}`;
|
||||
const failureCategory = payload.failureCategory || classifyLockBackendError(combinedOutput);
|
||||
|
||||
return {
|
||||
code: preflight.code,
|
||||
payload,
|
||||
relayList,
|
||||
failureCategory,
|
||||
stderrExcerpt: excerptText(preflight.stderr),
|
||||
stdoutExcerpt: excerptText(preflight.stdout),
|
||||
};
|
||||
}
|
||||
|
||||
export async function acquireLockWithRetry({ selectedAgent, cadence, platform, model, lockRetry, idempotencyKey }) {
|
||||
const lockCommandArgs = ['run', 'lock:lock', '--', '--agent', selectedAgent, '--cadence', cadence];
|
||||
if (model) {
|
||||
lockCommandArgs.push('--model', model);
|
||||
}
|
||||
const backoffScheduleMs = [];
|
||||
let attempts = 0;
|
||||
const correlationId = idempotencyKey || `${cadence}:${selectedAgent}:${getRunDateKey()}`;
|
||||
|
||||
while (true) {
|
||||
attempts += 1;
|
||||
const result = await runCommand('npm', lockCommandArgs, {
|
||||
env: {
|
||||
AGENT_PLATFORM: platform,
|
||||
...(model ? { AGENT_MODEL: model } : {}),
|
||||
...(idempotencyKey ? { SCHEDULER_LOCK_IDEMPOTENCY_KEY: idempotencyKey } : {}),
|
||||
SCHEDULER_LOCK_CORRELATION_ID: correlationId,
|
||||
SCHEDULER_LOCK_ATTEMPT_ID: String(attempts),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.code !== 2) {
|
||||
return { result, attempts, backoffScheduleMs, correlationId };
|
||||
}
|
||||
|
||||
if (attempts > lockRetry.maxRetries) {
|
||||
return {
|
||||
result,
|
||||
attempts,
|
||||
backoffScheduleMs,
|
||||
correlationId,
|
||||
finalBackendCategory: classifyLockBackendError(`${result.stderr}\n${result.stdout}`),
|
||||
};
|
||||
}
|
||||
|
||||
const exponentialBase = lockRetry.backoffMs * (2 ** (attempts - 1));
|
||||
const jitter = lockRetry.jitterMs > 0
|
||||
? Math.floor(Math.random() * (lockRetry.jitterMs + 1))
|
||||
: 0;
|
||||
const delayMs = exponentialBase + jitter;
|
||||
backoffScheduleMs.push(delayMs);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
event: 'scheduler.lock.retry',
|
||||
attempt: attempts,
|
||||
max_retries: lockRetry.maxRetries,
|
||||
delay_ms: delayMs,
|
||||
correlation_id: correlationId,
|
||||
selected_agent: selectedAgent,
|
||||
cadence,
|
||||
}));
|
||||
|
||||
if (delayMs > 0) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function summarizeLockFailureReasons(outputText) {
|
||||
const events = parseJsonEventsFromOutput(outputText);
|
||||
const quorumFailure = events.findLast((entry) => entry.event === 'lock_publish_quorum_failed');
|
||||
if (quorumFailure && quorumFailure.reasonDistribution && typeof quorumFailure.reasonDistribution === 'object') {
|
||||
return {
|
||||
reasonDistribution: quorumFailure.reasonDistribution,
|
||||
attemptId: quorumFailure.attemptId || null,
|
||||
correlationId: quorumFailure.correlationId || null,
|
||||
totalElapsedMs: quorumFailure.totalElapsedMs ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const distribution = {};
|
||||
for (const entry of events) {
|
||||
if (entry.event !== 'lock_publish_failure') continue;
|
||||
const reason = typeof entry.reason === 'string' && entry.reason.trim()
|
||||
? entry.reason.trim()
|
||||
: 'unknown';
|
||||
distribution[reason] = (distribution[reason] || 0) + 1;
|
||||
}
|
||||
return {
|
||||
reasonDistribution: distribution,
|
||||
attemptId: null,
|
||||
correlationId: null,
|
||||
totalElapsedMs: null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const ALLOWED_ENV_KEYS = new Set([
|
||||
'PATH', 'Path', // Windows compatibility
|
||||
'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ',
|
||||
'NODE_ENV', 'NODE_OPTIONS', 'NODE_PATH',
|
||||
'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'SystemRoot', 'windir', 'ComSpec', 'PATHEXT',
|
||||
'TMPDIR', 'TEMP', 'TMP',
|
||||
'EDITOR', 'VISUAL',
|
||||
// CI variables
|
||||
'CI', 'GITHUB_ACTIONS', 'GITHUB_REF', 'GITHUB_HEAD_REF', 'GITHUB_BASE_REF', 'GITHUB_EVENT_NAME', 'GITHUB_SHA',
|
||||
]);
|
||||
|
||||
export function getSafeEnv() {
|
||||
const safeEnv = {};
|
||||
for (const key in process.env) {
|
||||
if (ALLOWED_ENV_KEYS.has(key) ||
|
||||
key.startsWith('npm_') ||
|
||||
key.startsWith('NOSTR_') ||
|
||||
key.startsWith('TORCH_') ||
|
||||
key.startsWith('SCHEDULER_') ||
|
||||
key.startsWith('AGENT_')) {
|
||||
safeEnv[key] = process.env[key];
|
||||
}
|
||||
}
|
||||
return safeEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a child process with a sanitized environment by default.
|
||||
* @param {string} command
|
||||
* @param {string[]} args
|
||||
* @param {object} options
|
||||
* @param {object} [options.env] - Additional environment variables to merge.
|
||||
* @param {boolean} [options.inheritProcessEnv=false] - If true, use full process.env. If false (default), use sanitized env.
|
||||
*/
|
||||
export async function runCommand(command, args = [], options = {}) {
|
||||
const baseEnv = options.inheritProcessEnv ? process.env : getSafeEnv();
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: process.cwd(),
|
||||
env: { ...baseEnv, ...(options.env || {}) },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
const OUTPUT_LIMIT = 20000; // 20KB
|
||||
let stdoutWritten = 0;
|
||||
let stderrWritten = 0;
|
||||
let stdoutTruncated = false;
|
||||
let stderrTruncated = false;
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stdout += text;
|
||||
|
||||
if (stdoutWritten < OUTPUT_LIMIT) {
|
||||
const remaining = OUTPUT_LIMIT - stdoutWritten;
|
||||
if (text.length <= remaining) {
|
||||
process.stdout.write(text);
|
||||
stdoutWritten += text.length;
|
||||
} else {
|
||||
process.stdout.write(text.slice(0, remaining));
|
||||
stdoutWritten += remaining;
|
||||
}
|
||||
} else if (!stdoutTruncated) {
|
||||
process.stdout.write('\n...[stdout truncated]...\n');
|
||||
stdoutTruncated = true;
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
|
||||
if (stderrWritten < OUTPUT_LIMIT) {
|
||||
const remaining = OUTPUT_LIMIT - stderrWritten;
|
||||
if (text.length <= remaining) {
|
||||
process.stderr.write(text);
|
||||
stderrWritten += text.length;
|
||||
} else {
|
||||
process.stderr.write(text.slice(0, remaining));
|
||||
stderrWritten += remaining;
|
||||
}
|
||||
} else if (!stderrTruncated) {
|
||||
process.stderr.write('\n...[stderr truncated]...\n');
|
||||
stderrTruncated = true;
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({ code: code ?? 1, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function parseJsonFromOutput(text) {
|
||||
const trimmed = String(text || '').trim();
|
||||
if (!trimmed) return null;
|
||||
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||||
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
||||
const line = lines[i];
|
||||
if (!line.startsWith('{') && !line.startsWith('[')) continue;
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
// Keep scanning from the end.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseJsonEventsFromOutput(text) {
|
||||
const events = [];
|
||||
for (const line of String(text || '').split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('{')) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
events.push(parsed);
|
||||
}
|
||||
} catch {
|
||||
// Ignore non-JSON lines.
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
export async function readJson(filePath, fallback) {
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeStringList(value, fallback = []) {
|
||||
if (!Array.isArray(value)) return fallback;
|
||||
return value.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim());
|
||||
}
|
||||
|
||||
export function parseNonNegativeInt(value, fallback) {
|
||||
const parsed = Number.parseInt(String(value ?? ''), 10);
|
||||
if (Number.isFinite(parsed) && parsed >= 0) {
|
||||
return parsed;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function parseBooleanFlag(value, fallback = false) {
|
||||
if (value === undefined || value === null || value === '') return fallback;
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
||||
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function redactSensitive(text) {
|
||||
if (!text) return '';
|
||||
return String(text)
|
||||
.replace(/\b(BEARER\s+)[A-Za-z0-9._~+/=-]+/gi, '$1[REDACTED]')
|
||||
.replace(/\b(token|api[_-]?key|secret(?:[_-]?key)?|password|passwd|authorization)\s*[:=]\s*[^\s,;]+/gi, '$1=[REDACTED]')
|
||||
.replace(/\b(sk|pk|ghp|xoxb|xoxp)_[A-Za-z0-9_-]+\b/g, '[REDACTED]');
|
||||
}
|
||||
|
||||
export function excerptText(text, maxChars = 600) {
|
||||
const clean = redactSensitive(String(text || '').trim());
|
||||
if (!clean) return '';
|
||||
if (clean.length <= maxChars) return clean;
|
||||
return `${clean.slice(0, maxChars)}…`;
|
||||
}
|
||||
|
||||
export function formatDurationMs(ms) {
|
||||
if (!Number.isFinite(ms) || ms < 0) return 'unknown';
|
||||
const minutes = Math.round(ms / 60000);
|
||||
return `${minutes} minute(s)`;
|
||||
}
|
||||
|
||||
export function getRunDateKey(now = new Date()) {
|
||||
return now.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function toYamlScalar(value) {
|
||||
const str = String(value ?? '');
|
||||
return `'${str.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
export function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -182,10 +182,16 @@ async function main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const promises = entries.map(async (entry) => {
|
||||
const fullPath = path.join(cadenceDir, entry);
|
||||
logsScanned += 1;
|
||||
const record = await readLogFile(fullPath, cadence, cutoffMs);
|
||||
return record;
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
logsScanned += entries.length;
|
||||
|
||||
for (const record of results) {
|
||||
if (!record) continue;
|
||||
|
||||
records.push(record);
|
||||
|
||||
@@ -158,7 +158,8 @@ function extractIdentifiers(content) {
|
||||
const ids = new Set();
|
||||
const lines = content.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (/(issue|incident|failure)[-_\s]*id/i.test(line)) {
|
||||
// Match "failure id" as a distinct word phrase (ignores "failure identifiers" or "failure IDs")
|
||||
if (/(?:issue|incident|failure)[-_\s]*id\b/i.test(line)) {
|
||||
const match = line.match(/([A-Za-z0-9._-]{3,})\s*$/);
|
||||
if (match) ids.add(match[1]);
|
||||
}
|
||||
@@ -242,8 +243,19 @@ async function checkFailureTracking({ sinceMs }) {
|
||||
|
||||
for (const file of recentTestLogs) {
|
||||
const content = await fs.readFile(file.filePath, 'utf8').catch(() => '');
|
||||
if (FAILURE_KEYWORD_RE.test(content)) {
|
||||
const ids = extractIdentifiers(content);
|
||||
// Filter out passing TAP lines, comments, and YAML blocks to avoid false positives on test descriptions
|
||||
const relevantContent = content.split(/\r?\n/)
|
||||
.filter((line) => {
|
||||
const trimmed = line.trim();
|
||||
return !/^ok\s+\d+/.test(trimmed) &&
|
||||
!trimmed.startsWith('#') &&
|
||||
!trimmed.startsWith('>') &&
|
||||
!/^(failureType|code|name|stack|operator|expected|actual):/.test(trimmed);
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
if (FAILURE_KEYWORD_RE.test(relevantContent)) {
|
||||
const ids = extractIdentifiers(content); // Extract from full content to find IDs anywhere
|
||||
if (!ids.size) {
|
||||
logsWithFailureNoIds.push(path.relative(process.cwd(), file.filePath));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
+16
-7
@@ -7,11 +7,20 @@ import { getRoster } from './roster.mjs';
|
||||
import { queryLocks } from './lock-ops.mjs';
|
||||
import { todayDateStr } from './utils.mjs';
|
||||
|
||||
export async function cmdCheck(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const config = loadTorchConfig();
|
||||
export async function cmdCheck(cadence, deps = {}) {
|
||||
const {
|
||||
loadTorchConfigFn = loadTorchConfig,
|
||||
getRelaysFn = getRelays,
|
||||
getNamespaceFn = getNamespace,
|
||||
getRosterFn = getRoster,
|
||||
queryLocksFn = queryLocks,
|
||||
todayDateStrFn = todayDateStr,
|
||||
} = deps;
|
||||
|
||||
const relays = await getRelaysFn();
|
||||
const namespace = await getNamespaceFn();
|
||||
const dateStr = todayDateStrFn();
|
||||
const config = await loadTorchConfigFn();
|
||||
const pausedAgents = (cadence === 'daily' ? config.scheduler.paused.daily : config.scheduler.paused.weekly) || [];
|
||||
|
||||
console.error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`);
|
||||
@@ -20,9 +29,9 @@ export async function cmdCheck(cadence) {
|
||||
console.error(`Paused agents: ${pausedAgents.join(', ')}`);
|
||||
}
|
||||
|
||||
const locks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const locks = await queryLocksFn(relays, cadence, dateStr, namespace);
|
||||
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
|
||||
const roster = getRoster(cadence);
|
||||
const roster = await getRosterFn(cadence);
|
||||
|
||||
const excludedAgents = [...new Set([...lockedAgents, ...pausedAgents])];
|
||||
const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent));
|
||||
|
||||
+3
-3
@@ -7,8 +7,8 @@ import { queryLocks } from './lock-ops.mjs';
|
||||
import { todayDateStr, nowUnix } from './utils.mjs';
|
||||
|
||||
export async function cmdList(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const cadences = cadence ? [cadence] : ['daily', 'weekly'];
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function cmdList(cadence) {
|
||||
);
|
||||
}
|
||||
|
||||
const roster = getRoster(c);
|
||||
const roster = await getRoster(c);
|
||||
const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean));
|
||||
const unknownLockedAgents = [...lockedAgents].filter((agent) => !roster.includes(agent));
|
||||
const available = roster.filter((a) => !lockedAgents.has(a));
|
||||
|
||||
@@ -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 = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const ttl = 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 = 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 };
|
||||
}
|
||||
@@ -11,5 +11,57 @@ export const DEFAULT_QUERY_TIMEOUT_MS = 30_000;
|
||||
export const DEFAULT_PUBLISH_TIMEOUT_MS = 15_000;
|
||||
export const DEFAULT_MIN_SUCCESSFUL_PUBLISHES = 1;
|
||||
export const DEFAULT_MIN_ACTIVE_RELAY_POOL = 1;
|
||||
export const DEFAULT_RETRY_ATTEMPTS = 4;
|
||||
export const DEFAULT_RETRY_BASE_DELAY_MS = 500;
|
||||
export const DEFAULT_RETRY_CAP_DELAY_MS = 8000;
|
||||
export const DEFAULT_ROLLING_WINDOW_SIZE = 25;
|
||||
export const DEFAULT_FAILURE_THRESHOLD = 3;
|
||||
export const DEFAULT_QUARANTINE_COOLDOWN_MS = 30_000;
|
||||
export const DEFAULT_MAX_QUARANTINE_COOLDOWN_MS = 300_000;
|
||||
export const DEFAULT_SNAPSHOT_INTERVAL_MS = 60_000;
|
||||
export const VALID_CADENCES = new Set(['daily', 'weekly']);
|
||||
export const KIND_APP_DATA = 30078;
|
||||
|
||||
export const USAGE_TEXT = `Usage: torch-lock <command> [options]
|
||||
|
||||
Commands:
|
||||
check --cadence <daily|weekly> Check locked agents (JSON)
|
||||
lock --agent <name> --cadence <daily|weekly> Claim a lock
|
||||
complete --agent <name> --cadence <daily|weekly> Mark task as completed (permanent)
|
||||
list [--cadence <daily|weekly>] Print active lock table
|
||||
health --cadence <daily|weekly> Probe relay websocket + publish/read health
|
||||
dashboard [--port <port>] [--host <host>] Serve the dashboard (default: ${DEFAULT_DASHBOARD_PORT})
|
||||
init [--force] Initialize torch/ directory in current project
|
||||
update [--force] Update torch/ configuration (backups, merges)
|
||||
|
||||
list-memories [--agent <id>] [--type <type>] [--tags <a,b>] [--pinned <true|false>] [--full]
|
||||
inspect-memory --id <memoryId>
|
||||
pin-memory --id <memoryId>
|
||||
unpin-memory --id <memoryId>
|
||||
trigger-prune-dry-run [--retention-ms <ms>]
|
||||
memory-stats [--window-ms <ms>]
|
||||
|
||||
Options:
|
||||
--dry-run Build and sign the event but do not publish
|
||||
--force Overwrite existing files (for init) or all files (for update)
|
||||
--log-dir Path to task logs directory (default: task-logs)
|
||||
--ignore-logs Skip checking local logs for completed tasks
|
||||
--json Emit compact single-line JSON
|
||||
--json-file Write JSON output to a file path
|
||||
--quiet Suppress stderr progress logs (pairs well with --json)
|
||||
|
||||
Environment:
|
||||
NOSTR_LOCK_NAMESPACE Namespace prefix for lock tags (default: torch)
|
||||
NOSTR_LOCK_RELAYS Comma-separated relay WSS URLs
|
||||
NOSTR_LOCK_TTL Lock TTL in seconds (default: 7200)
|
||||
NOSTR_LOCK_QUERY_TIMEOUT_MS Relay query timeout in milliseconds (default: 15000)
|
||||
NOSTR_LOCK_DAILY_ROSTER Comma-separated daily roster (optional)
|
||||
NOSTR_LOCK_WEEKLY_ROSTER Comma-separated weekly roster (optional)
|
||||
TORCH_CONFIG_PATH Optional path to torch-config.json (default: ./torch-config.json)
|
||||
AGENT_PLATFORM Platform identifier (e.g., codex)
|
||||
|
||||
Exit codes:
|
||||
0 Success
|
||||
1 Usage error
|
||||
2 Relay/network error
|
||||
3 Lock denied (already locked or race lost)`;
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
# Context
|
||||
Goal: Improve CI reliability and developer confidence.
|
||||
Scope: Local test execution and flake detection.
|
||||
Constraints: No external CI access (GitHub Actions). Run tests locally.
|
||||
|
||||
- agent: ci-health-agent
|
||||
- cadence: daily
|
||||
- session: 2026-02-17T19-36-32Z
|
||||
- run-start: 2026-02-17T19-36-32Z
|
||||
- prompt-path: src/prompts/daily/ci-health-agent.md
|
||||
@@ -0,0 +1,10 @@
|
||||
# Context for Constants Refactor Run
|
||||
|
||||
**Agent:** const-refactor-agent
|
||||
**Date:** 2026-02-17T19-41-31Z
|
||||
**Baseline:**
|
||||
- `js/` directory does not exist. Scanning `src/` instead.
|
||||
- `AGENTS.md` and `CLAUDE.md` not found.
|
||||
- Target branch: default
|
||||
|
||||
**Goal:** Find duplicated numeric constants in `src/` and extract them to `src/constants.mjs`.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
agent: content-audit-agent
|
||||
cadence: daily
|
||||
run-start: 2026-02-17T21:29:25Z
|
||||
prompt-path: src/prompts/daily/content-audit-agent.md
|
||||
---
|
||||
|
||||
# Audit Context
|
||||
|
||||
## Goal
|
||||
Audit the `/content` directory for user-facing documentation accuracy, specifically focusing on upload flows and contribution guidelines.
|
||||
|
||||
## Scope
|
||||
- Directory: `/content` (if exists)
|
||||
- Docs: Upload guides, contribution docs.
|
||||
- Code: Related upload handlers and config.
|
||||
|
||||
## Constraints
|
||||
- Do not invent APIs.
|
||||
- Prefer docs changes over code changes.
|
||||
- Exit without changes if no work is required (or if target is missing).
|
||||
@@ -0,0 +1,34 @@
|
||||
# Context: Decompose scripts/agent/run-scheduler-cycle.mjs
|
||||
|
||||
- **Chosen File**: `scripts/agent/run-scheduler-cycle.mjs`
|
||||
- **Line Count**: 1127 lines
|
||||
- **Reason**: Largest source file in the repository (excluding generated/vendored/test logs). No substantial refactor found in recent history (last commit was a merge).
|
||||
- **Goal**: Reduce file size by extracting cohesive blocks into new modules.
|
||||
- **Node Version**: v22.22.0 (as seen in `npm run lock:check:daily` output previously)
|
||||
- **Extraction Plan**:
|
||||
1. Extract low-level utilities to `scripts/agent/scheduler-utils.mjs`.
|
||||
2. Extract lock-related logic to `scripts/agent/scheduler-lock.mjs`.
|
||||
|
||||
## Proposed Extractions
|
||||
|
||||
### 1. `scripts/agent/scheduler-utils.mjs`
|
||||
This will house generic helpers used throughout the script, reducing the noise in the main logic.
|
||||
- `runCommand`
|
||||
- `getSafeEnv`
|
||||
- `sleep`
|
||||
- `parseJsonFromOutput`
|
||||
- `parseJsonEventsFromOutput`
|
||||
- `redactSensitive`
|
||||
- `excerptText`
|
||||
- `parseNonNegativeInt`
|
||||
- `parseBooleanFlag`
|
||||
- `readJson`
|
||||
- `normalizeStringList`
|
||||
|
||||
### 2. `scripts/agent/scheduler-lock.mjs`
|
||||
This will encapsulate the complex locking logic.
|
||||
- `acquireLockWithRetry`
|
||||
- `summarizeLockFailureReasons`
|
||||
- `classifyLockBackendError`
|
||||
- `buildLockBackendRemediation`
|
||||
- `runLockHealthPreflight`
|
||||
@@ -0,0 +1,16 @@
|
||||
# Deps Security Agent Context
|
||||
|
||||
- Agent: deps-security-agent
|
||||
- Cadence: daily
|
||||
- Run Start: 2026-02-17T23-13-04Z
|
||||
- Prompt: src/prompts/daily/deps-security-agent.md
|
||||
|
||||
## Goal
|
||||
Perform daily security and dependency audit.
|
||||
|
||||
## Scope
|
||||
Reviewing dependencies for security and freshness. `nostr-tools` and `eslint`.
|
||||
|
||||
## Constraints
|
||||
- Do not auto-upgrade protocol libraries.
|
||||
- Do not auto-upgrade if versions are anomalous.
|
||||
@@ -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.
|
||||
+43
-21
@@ -22,9 +22,10 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
// Resolve package root relative to this file (src/dashboard.mjs)
|
||||
// this file is in <root>/src/dashboard.mjs, so '..' goes to <root>
|
||||
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const auth = getDashboardAuth();
|
||||
const auth = await getDashboardAuth();
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
// Basic Auth check
|
||||
if (auth) {
|
||||
const authHeader = req.headers.authorization;
|
||||
@@ -104,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);
|
||||
@@ -127,14 +138,17 @@ 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 = [
|
||||
path.join(packageRoot, 'dashboard'),
|
||||
path.join(packageRoot, 'assets'),
|
||||
path.join(packageRoot, 'src', 'docs'),
|
||||
path.join(packageRoot, 'src', 'constants.mjs'),
|
||||
path.join(packageRoot, 'torch-config.json')
|
||||
];
|
||||
|
||||
@@ -181,20 +195,28 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
const listenUrl = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/dashboard/`;
|
||||
console.log(`Dashboard running at ${listenUrl}`);
|
||||
if (auth) {
|
||||
console.log('Authentication: enabled (Basic Auth)');
|
||||
} else {
|
||||
console.warn('Authentication: DISABLED (Dashboard is public)');
|
||||
} catch (err) {
|
||||
console.error('Dashboard Server Error:', err);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500);
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
}
|
||||
console.log(`Serving files from ${packageRoot}`);
|
||||
console.log(`Using configuration from ${process.cwd()}`);
|
||||
});
|
||||
|
||||
// Keep process alive
|
||||
return server;
|
||||
return new Promise((resolve, reject) => {
|
||||
server.listen(port, host, () => {
|
||||
const listenUrl = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/dashboard/`;
|
||||
console.log(`Dashboard running at ${listenUrl}`);
|
||||
if (auth) {
|
||||
console.log('Authentication: enabled (Basic Auth)');
|
||||
} else {
|
||||
console.warn('Authentication: DISABLED (Dashboard is public)');
|
||||
}
|
||||
console.log(`Serving files from ${packageRoot}`);
|
||||
console.log(`Using configuration from ${process.cwd()}`);
|
||||
resolve(server);
|
||||
});
|
||||
server.on('error', reject);
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
# Decisions
|
||||
Decision: Run local tests to simulate CI environment.
|
||||
Rationale: No access to GitHub Actions API.
|
||||
|
||||
- agent: ci-health-agent
|
||||
- cadence: daily
|
||||
- session: 2026-02-17T19-36-32Z
|
||||
- run-start: 2026-02-17T19-36-32Z
|
||||
- prompt-path: src/prompts/daily/ci-health-agent.md
|
||||
@@ -0,0 +1,3 @@
|
||||
- Extracted retry/health constants from `src/lock-ops.mjs` to `src/constants.mjs`.
|
||||
- Constants: `DEFAULT_RETRY_ATTEMPTS`, `DEFAULT_RETRY_BASE_DELAY_MS`, `DEFAULT_RETRY_CAP_DELAY_MS`, `DEFAULT_ROLLING_WINDOW_SIZE`, `DEFAULT_FAILURE_THRESHOLD`, `DEFAULT_QUARANTINE_COOLDOWN_MS`, `DEFAULT_MAX_QUARANTINE_COOLDOWN_MS`, `DEFAULT_SNAPSHOT_INTERVAL_MS`.
|
||||
- Reason: These values were hardcoded in multiple places in `src/lock-ops.mjs` and represent system-wide defaults.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
agent: content-audit-agent
|
||||
cadence: daily
|
||||
run-start: 2026-02-17T21:29:25Z
|
||||
prompt-path: src/prompts/daily/content-audit-agent.md
|
||||
---
|
||||
|
||||
# Decisions
|
||||
|
||||
## Decision: Report missing content directory
|
||||
- **Rationale**: The agent is tasked with auditing `/content`, but the directory does not exist in the repository root. This is likely a configuration error or the content is located elsewhere (e.g., `src/docs`, but the prompt specifically targets `/content`). The decision is to report this as a Known Issue and not attempt to audit unrelated directories to avoid incorrect assumptions.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Decisions: Decompose scripts/agent/run-scheduler-cycle.mjs
|
||||
|
||||
## Rationale
|
||||
The `scripts/agent/run-scheduler-cycle.mjs` file had grown to 1127 lines, making it the largest source file in the repository. It contained mixed responsibilities: process management, logging, locking logic, and configuration parsing.
|
||||
|
||||
We decided to extract cohesive blocks to improve maintainability and testability.
|
||||
|
||||
## Extracted Modules
|
||||
|
||||
### 1. `scripts/agent/scheduler-utils.mjs`
|
||||
**Why:** Many utility functions (`runCommand`, `getSafeEnv`, `parseJson*`, `sleep`, etc.) were cluttering the main logic. These are generic and can be reused or at least moved out of the way.
|
||||
**Changes:** Moved low-level helpers here. Exported them.
|
||||
|
||||
### 2. `scripts/agent/scheduler-lock.mjs`
|
||||
**Why:** The locking logic (`acquireLockWithRetry`, failure classification, remediation) is complex and distinct from the core scheduler loop. It forms a "locking service" conceptual block.
|
||||
**Changes:** Moved `acquireLockWithRetry`, `classifyLockBackendError`, `buildLockBackendRemediation`, `runLockHealthPreflight`, and `summarizeLockFailureReasons` here.
|
||||
|
||||
## Baseline Update
|
||||
The `scripts/check-file-size.mjs` script does not currently support an `--update` flag or contain a `BASELINE` object as referenced in the agent prompt. Therefore, no code change was made to that script. However, the file size reduction was verified manually:
|
||||
- Original: 1127 lines
|
||||
- New: 788 lines
|
||||
- Reduction: 339 lines (exceeds 200 line target).
|
||||
|
||||
## Test Updates
|
||||
The E2E tests (`test/scheduler-preflight-lock.e2e.test.mjs`, `test/run-scheduler-cycle-memory-policy.test.mjs`, `test/scheduler-lock-failure-schema.contract.test.mjs`) required updates to copy the new module files (`scheduler-utils.mjs`, `scheduler-lock.mjs`) into the test fixture directory.
|
||||
The unit test `test/scheduler-ratchet.test.mjs` was updated to look for `classifyLockBackendError` in the new `scheduler-lock.mjs` file instead of the main script.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Decisions
|
||||
|
||||
- Agent: deps-security-agent
|
||||
- Cadence: daily
|
||||
- Run Start: 2026-02-17T23-13-04Z
|
||||
- Prompt: src/prompts/daily/deps-security-agent.md
|
||||
|
||||
1. **Decision**: Skip `nostr-tools` auto-upgrade.
|
||||
**Rationale**: It is a protocol library and requires manual review per policy.
|
||||
2. **Decision**: Skip `eslint` changes.
|
||||
**Rationale**: The installed version (v10) and audit suggestions are conflicting. Needs human investigation.
|
||||
@@ -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.
|
||||
+17
-59
@@ -14,10 +14,10 @@ import {
|
||||
getHashtag as _getHashtag,
|
||||
} from './torch-config.mjs';
|
||||
import {
|
||||
DEFAULT_DASHBOARD_PORT,
|
||||
VALID_CADENCES,
|
||||
KIND_APP_DATA,
|
||||
RACE_CHECK_DELAY_MS,
|
||||
USAGE_TEXT,
|
||||
} from './constants.mjs';
|
||||
import { cmdInit, cmdUpdate } from './ops.mjs';
|
||||
import { parseArgs } from './cli-parser.mjs';
|
||||
@@ -75,10 +75,10 @@ export async function cmdCheck(cadence, deps = {}) {
|
||||
quiet = false,
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const dateStr = getDateStr();
|
||||
const config = loadTorchConfig();
|
||||
const config = await loadTorchConfig();
|
||||
const pausedAgents = config.scheduler.paused[cadence] || [];
|
||||
|
||||
if (!quiet) {
|
||||
@@ -99,7 +99,7 @@ export async function cmdCheck(cadence, deps = {}) {
|
||||
|
||||
const locks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
|
||||
const roster = getRoster(cadence);
|
||||
const roster = await getRoster(cadence);
|
||||
const rosterSet = new Set(roster);
|
||||
|
||||
const excludedAgentsSet = new Set([...lockedAgents, ...pausedAgents, ...completedAgents]);
|
||||
@@ -180,11 +180,11 @@ export async function cmdLock(agent, cadence, optionsOrDryRun = false, deps = {}
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const hashtag = getHashtag();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const hashtag = await getHashtag();
|
||||
const dateStr = getDateStr();
|
||||
const ttl = getTtl();
|
||||
const ttl = await getTtl();
|
||||
const now = nowUnix();
|
||||
const expiresAt = now + ttl;
|
||||
|
||||
@@ -193,7 +193,7 @@ export async function cmdLock(agent, cadence, optionsOrDryRun = false, deps = {}
|
||||
error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
|
||||
error(`Relays: ${relays.join(', ')}`);
|
||||
|
||||
const roster = getRoster(cadence);
|
||||
const roster = await getRoster(cadence);
|
||||
if (!roster.includes(agent)) {
|
||||
error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
||||
error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
|
||||
@@ -319,8 +319,8 @@ export async function cmdList(cadence, deps = {}) {
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const dateStr = getDateStr();
|
||||
const cadences = cadence ? [cadence] : [...VALID_CADENCES];
|
||||
|
||||
@@ -365,7 +365,7 @@ export async function cmdList(cadence, deps = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
const roster = getRoster(c);
|
||||
const roster = await getRoster(c);
|
||||
const rosterSet = new Set(roster);
|
||||
const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean));
|
||||
const unknownLockedAgents = [...lockedAgents].filter((agent) => !rosterSet.has(agent));
|
||||
@@ -413,9 +413,9 @@ export async function cmdComplete(agent, cadence, optionsOrDryRun = false, deps
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const hashtag = getHashtag();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const hashtag = await getHashtag();
|
||||
const dateStr = getDateStr();
|
||||
const now = nowUnix();
|
||||
|
||||
@@ -492,49 +492,7 @@ export async function cmdComplete(agent, cadence, optionsOrDryRun = false, deps
|
||||
}
|
||||
|
||||
function usage() {
|
||||
console.error(`Usage: torch-lock <command> [options]
|
||||
|
||||
Commands:
|
||||
check --cadence <daily|weekly> Check locked agents (JSON)
|
||||
lock --agent <name> --cadence <daily|weekly> Claim a lock
|
||||
complete --agent <name> --cadence <daily|weekly> Mark task as completed (permanent)
|
||||
list [--cadence <daily|weekly>] Print active lock table
|
||||
health --cadence <daily|weekly> Probe relay websocket + publish/read health
|
||||
dashboard [--port <port>] [--host <host>] Serve the dashboard (default: ${DEFAULT_DASHBOARD_PORT})
|
||||
init [--force] Initialize torch/ directory in current project
|
||||
update [--force] Update torch/ configuration (backups, merges)
|
||||
|
||||
list-memories [--agent <id>] [--type <type>] [--tags <a,b>] [--pinned <true|false>] [--full]
|
||||
inspect-memory --id <memoryId>
|
||||
pin-memory --id <memoryId>
|
||||
unpin-memory --id <memoryId>
|
||||
trigger-prune-dry-run [--retention-ms <ms>]
|
||||
memory-stats [--window-ms <ms>]
|
||||
|
||||
Options:
|
||||
--dry-run Build and sign the event but do not publish
|
||||
--force Overwrite existing files (for init) or all files (for update)
|
||||
--log-dir Path to task logs directory (default: task-logs)
|
||||
--ignore-logs Skip checking local logs for completed tasks
|
||||
--json Emit compact single-line JSON
|
||||
--json-file Write JSON output to a file path
|
||||
--quiet Suppress stderr progress logs (pairs well with --json)
|
||||
|
||||
Environment:
|
||||
NOSTR_LOCK_NAMESPACE Namespace prefix for lock tags (default: torch)
|
||||
NOSTR_LOCK_RELAYS Comma-separated relay WSS URLs
|
||||
NOSTR_LOCK_TTL Lock TTL in seconds (default: 7200)
|
||||
NOSTR_LOCK_QUERY_TIMEOUT_MS Relay query timeout in milliseconds (default: 15000)
|
||||
NOSTR_LOCK_DAILY_ROSTER Comma-separated daily roster (optional)
|
||||
NOSTR_LOCK_WEEKLY_ROSTER Comma-separated weekly roster (optional)
|
||||
TORCH_CONFIG_PATH Optional path to torch-config.json (default: ./torch-config.json)
|
||||
AGENT_PLATFORM Platform identifier (e.g., codex)
|
||||
|
||||
Exit codes:
|
||||
0 Success
|
||||
1 Usage error
|
||||
2 Relay/network error
|
||||
3 Lock denied (already locked or race lost)`);
|
||||
console.error(USAGE_TEXT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+441
-269
@@ -1,3 +1,4 @@
|
||||
import { randomInt, randomUUID } from 'node:crypto';
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
import {
|
||||
getQueryTimeoutMs,
|
||||
@@ -6,12 +7,17 @@ import {
|
||||
getRelayFallbacks,
|
||||
getMinActiveRelayPool,
|
||||
} from './torch-config.mjs';
|
||||
import { KIND_APP_DATA } from './constants.mjs';
|
||||
|
||||
const relayHealthState = {
|
||||
metricsByRelay: new Map(),
|
||||
lastSnapshotAt: 0,
|
||||
};
|
||||
import {
|
||||
KIND_APP_DATA,
|
||||
DEFAULT_RETRY_ATTEMPTS,
|
||||
DEFAULT_RETRY_BASE_DELAY_MS,
|
||||
DEFAULT_RETRY_CAP_DELAY_MS,
|
||||
DEFAULT_ROLLING_WINDOW_SIZE,
|
||||
DEFAULT_FAILURE_THRESHOLD,
|
||||
DEFAULT_QUARANTINE_COOLDOWN_MS,
|
||||
DEFAULT_MAX_QUARANTINE_COOLDOWN_MS,
|
||||
DEFAULT_SNAPSHOT_INTERVAL_MS,
|
||||
} from './constants.mjs';
|
||||
|
||||
function nowUnix() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
@@ -67,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')
|
||||
@@ -78,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')
|
||||
@@ -93,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')
|
||||
@@ -116,44 +139,33 @@ 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);
|
||||
}
|
||||
|
||||
function calculateBackoffDelayMs(attemptNumber, baseMs, capMs, randomFn = Math.random) {
|
||||
const maxDelay = Math.min(capMs, baseMs * (2 ** Math.max(0, attemptNumber - 1)));
|
||||
return Math.floor(randomFn() * maxDelay);
|
||||
const MAX_RANDOM = 281474976710655; // 2**48 - 1
|
||||
|
||||
function secureRandom() {
|
||||
return randomInt(0, MAX_RANDOM) / MAX_RANDOM;
|
||||
}
|
||||
|
||||
function ensureRelayMetrics(relay, config) {
|
||||
let metrics = relayHealthState.metricsByRelay.get(relay);
|
||||
if (!metrics) {
|
||||
metrics = {
|
||||
relay,
|
||||
recentOutcomes: [],
|
||||
failureStreak: 0,
|
||||
quarantineUntil: 0,
|
||||
cooldownMs: config.quarantineCooldownMs,
|
||||
lastLatencyMs: null,
|
||||
lastResultAt: null,
|
||||
};
|
||||
relayHealthState.metricsByRelay.set(relay, metrics);
|
||||
}
|
||||
return metrics;
|
||||
function calculateBackoffDelayMs(attemptNumber, baseMs, capMs, randomFn = secureRandom) {
|
||||
const maxDelay = Math.min(capMs, baseMs * (2 ** Math.max(0, attemptNumber - 1)));
|
||||
return Math.floor(randomFn() * maxDelay);
|
||||
}
|
||||
|
||||
function summarizeRelayMetrics(metrics, nowMs) {
|
||||
@@ -189,103 +201,180 @@ function computeRelayScore(summary) {
|
||||
return (summary.successRate * 1.4) - (summary.timeoutRate * 0.9) - latencyPenalty - quarantinePenalty;
|
||||
}
|
||||
|
||||
function rankRelaysByHealth(relays, config, nowMs = Date.now()) {
|
||||
const summaries = relays.map((relay) => {
|
||||
const metrics = ensureRelayMetrics(relay, config);
|
||||
const summary = summarizeRelayMetrics(metrics, nowMs);
|
||||
export class RelayHealthManager {
|
||||
constructor() {
|
||||
this.metricsByRelay = new Map();
|
||||
this.lastSnapshotAt = 0;
|
||||
this._metricsVersion = 0;
|
||||
this._sortedCache = null;
|
||||
}
|
||||
|
||||
ensureMetrics(relay, config) {
|
||||
let metrics = this.metricsByRelay.get(relay);
|
||||
if (!metrics) {
|
||||
metrics = {
|
||||
relay,
|
||||
recentOutcomes: [],
|
||||
failureStreak: 0,
|
||||
quarantineUntil: 0,
|
||||
cooldownMs: config.quarantineCooldownMs,
|
||||
lastLatencyMs: null,
|
||||
lastResultAt: null,
|
||||
};
|
||||
this.metricsByRelay.set(relay, metrics);
|
||||
this._metricsVersion += 1;
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
|
||||
rankRelays(relays, config, nowMs = Date.now()) {
|
||||
for (const relay of relays) {
|
||||
this.ensureMetrics(relay, config);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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()) {
|
||||
const minActive = Math.max(1, Math.min(config.minActiveRelayPool, relays.length || 1));
|
||||
const ranked = this.rankRelays([...new Set(relays)], config, nowMs);
|
||||
const active = ranked.filter((entry) => !entry.summary.quarantined);
|
||||
const quarantined = ranked
|
||||
.filter((entry) => entry.summary.quarantined)
|
||||
.sort((a, b) => a.summary.quarantineRemainingMs - b.summary.quarantineRemainingMs);
|
||||
|
||||
const selected = [...active];
|
||||
if (selected.length < minActive) {
|
||||
const additionalNeeded = Math.min(minActive - selected.length, quarantined.length);
|
||||
selected.push(...quarantined.slice(0, additionalNeeded));
|
||||
}
|
||||
|
||||
return {
|
||||
relay,
|
||||
summary,
|
||||
score: computeRelayScore(summary),
|
||||
prioritized: selected.map((entry) => entry.relay),
|
||||
ranked,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
recordOutcome(relay, success, errorMessage, latencyMs, config, nowMs = Date.now()) {
|
||||
const metrics = this.ensureMetrics(relay, config);
|
||||
const timedOut = String(errorMessage || '').toLowerCase().includes('timeout')
|
||||
|| String(errorMessage || '').toLowerCase().includes('timed out');
|
||||
metrics.recentOutcomes.push({ success, timedOut, latencyMs, atMs: nowMs });
|
||||
if (metrics.recentOutcomes.length > config.rollingWindowSize) {
|
||||
metrics.recentOutcomes.splice(0, metrics.recentOutcomes.length - config.rollingWindowSize);
|
||||
}
|
||||
return a.relay.localeCompare(b.relay);
|
||||
});
|
||||
}
|
||||
metrics.lastResultAt = nowMs;
|
||||
metrics.lastLatencyMs = Number.isFinite(latencyMs) ? latencyMs : null;
|
||||
|
||||
function prioritizeRelays(relays, config, nowMs = Date.now()) {
|
||||
const minActive = Math.max(1, Math.min(config.minActiveRelayPool, relays.length || 1));
|
||||
const ranked = rankRelaysByHealth([...new Set(relays)], config, nowMs);
|
||||
const active = ranked.filter((entry) => !entry.summary.quarantined);
|
||||
const quarantined = ranked
|
||||
.filter((entry) => entry.summary.quarantined)
|
||||
.sort((a, b) => a.summary.quarantineRemainingMs - b.summary.quarantineRemainingMs);
|
||||
|
||||
const selected = [...active];
|
||||
if (selected.length < minActive) {
|
||||
const additionalNeeded = Math.min(minActive - selected.length, quarantined.length);
|
||||
selected.push(...quarantined.slice(0, additionalNeeded));
|
||||
}
|
||||
|
||||
return {
|
||||
prioritized: selected.map((entry) => entry.relay),
|
||||
ranked,
|
||||
};
|
||||
}
|
||||
|
||||
function recordRelayOutcome(relay, success, errorMessage, latencyMs, config, nowMs = Date.now()) {
|
||||
const metrics = ensureRelayMetrics(relay, config);
|
||||
const timedOut = String(errorMessage || '').toLowerCase().includes('timeout')
|
||||
|| String(errorMessage || '').toLowerCase().includes('timed out');
|
||||
metrics.recentOutcomes.push({ success, timedOut, latencyMs, atMs: nowMs });
|
||||
if (metrics.recentOutcomes.length > config.rollingWindowSize) {
|
||||
metrics.recentOutcomes.splice(0, metrics.recentOutcomes.length - config.rollingWindowSize);
|
||||
}
|
||||
metrics.lastResultAt = nowMs;
|
||||
metrics.lastLatencyMs = Number.isFinite(latencyMs) ? latencyMs : null;
|
||||
|
||||
if (success) {
|
||||
metrics.failureStreak = 0;
|
||||
if (metrics.quarantineUntil > nowMs) {
|
||||
metrics.quarantineUntil = 0;
|
||||
metrics.cooldownMs = config.quarantineCooldownMs;
|
||||
if (success) {
|
||||
metrics.failureStreak = 0;
|
||||
if (metrics.quarantineUntil > nowMs) {
|
||||
metrics.quarantineUntil = 0;
|
||||
metrics.cooldownMs = config.quarantineCooldownMs;
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
|
||||
metrics.failureStreak += 1;
|
||||
if (metrics.failureStreak >= config.failureThreshold) {
|
||||
metrics.quarantineUntil = nowMs + metrics.cooldownMs;
|
||||
metrics.cooldownMs = Math.min(config.maxQuarantineCooldownMs, Math.floor(metrics.cooldownMs * 1.5));
|
||||
}
|
||||
this._metricsVersion += 1;
|
||||
}
|
||||
|
||||
metrics.failureStreak += 1;
|
||||
if (metrics.failureStreak >= config.failureThreshold) {
|
||||
metrics.quarantineUntil = nowMs + metrics.cooldownMs;
|
||||
metrics.cooldownMs = Math.min(config.maxQuarantineCooldownMs, Math.floor(metrics.cooldownMs * 1.5));
|
||||
collectSnapshot(relays, config, nowMs = Date.now()) {
|
||||
const uniqueRelays = [...new Set(relays)];
|
||||
return this.rankRelays(uniqueRelays, config, nowMs).map((entry) => ({
|
||||
relay: entry.relay,
|
||||
score: Number(entry.score.toFixed(4)),
|
||||
...entry.summary,
|
||||
}));
|
||||
}
|
||||
|
||||
maybeLogSnapshot(relays, config, logger, reason, force = false, nowMs = Date.now()) {
|
||||
const intervalReached = nowMs - this.lastSnapshotAt >= config.snapshotIntervalMs;
|
||||
if (!force && !intervalReached) return;
|
||||
this.lastSnapshotAt = nowMs;
|
||||
logger(JSON.stringify({
|
||||
event: 'relay_health_snapshot',
|
||||
reason,
|
||||
relays: this.collectSnapshot(relays, config, nowMs),
|
||||
}));
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.metricsByRelay.clear();
|
||||
this.lastSnapshotAt = 0;
|
||||
this._metricsVersion = 0;
|
||||
this._sortedCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
function collectHealthSnapshot(relays, config, nowMs = Date.now()) {
|
||||
const uniqueRelays = [...new Set(relays)];
|
||||
return rankRelaysByHealth(uniqueRelays, config, nowMs).map((entry) => ({
|
||||
relay: entry.relay,
|
||||
score: Number(entry.score.toFixed(4)),
|
||||
...entry.summary,
|
||||
}));
|
||||
}
|
||||
|
||||
function maybeLogHealthSnapshot(relays, config, logger, reason, force = false, nowMs = Date.now()) {
|
||||
const intervalReached = nowMs - relayHealthState.lastSnapshotAt >= config.snapshotIntervalMs;
|
||||
if (!force && !intervalReached) return;
|
||||
relayHealthState.lastSnapshotAt = nowMs;
|
||||
logger(JSON.stringify({
|
||||
event: 'relay_health_snapshot',
|
||||
reason,
|
||||
relays: collectHealthSnapshot(relays, config, nowMs),
|
||||
}));
|
||||
}
|
||||
export const defaultHealthManager = new RelayHealthManager();
|
||||
|
||||
function buildRelayHealthConfig(deps) {
|
||||
return {
|
||||
rollingWindowSize: deps.rollingWindowSize ?? 25,
|
||||
failureThreshold: deps.failureThreshold ?? 3,
|
||||
quarantineCooldownMs: deps.quarantineCooldownMs ?? 30_000,
|
||||
maxQuarantineCooldownMs: deps.maxQuarantineCooldownMs ?? 5 * 60_000,
|
||||
snapshotIntervalMs: deps.snapshotIntervalMs ?? 60_000,
|
||||
rollingWindowSize: deps.rollingWindowSize ?? DEFAULT_ROLLING_WINDOW_SIZE,
|
||||
failureThreshold: deps.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD,
|
||||
quarantineCooldownMs: deps.quarantineCooldownMs ?? DEFAULT_QUARANTINE_COOLDOWN_MS,
|
||||
maxQuarantineCooldownMs: deps.maxQuarantineCooldownMs ?? DEFAULT_MAX_QUARANTINE_COOLDOWN_MS,
|
||||
snapshotIntervalMs: deps.snapshotIntervalMs ?? DEFAULT_SNAPSHOT_INTERVAL_MS,
|
||||
minActiveRelayPool: Math.max(1, deps.minActiveRelayPool),
|
||||
};
|
||||
}
|
||||
@@ -302,19 +391,20 @@ export async function queryLocks(relays, cadence, dateStr, namespace, deps = {})
|
||||
getMinActiveRelayPoolFn = getMinActiveRelayPool,
|
||||
errorLogger = console.error,
|
||||
healthLogger = console.error,
|
||||
healthManager = defaultHealthManager,
|
||||
} = deps;
|
||||
|
||||
const pool = poolFactory();
|
||||
const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`;
|
||||
const queryTimeoutMs = getQueryTimeoutMsFn();
|
||||
const fallbackRelays = getRelayFallbacksFn().filter((relay) => !relays.includes(relay));
|
||||
const queryTimeoutMs = await getQueryTimeoutMsFn();
|
||||
const fallbackRelays = (await getRelayFallbacksFn()).filter((relay) => !relays.includes(relay));
|
||||
const healthConfig = buildRelayHealthConfig({
|
||||
...deps,
|
||||
minActiveRelayPool: getMinActiveRelayPoolFn(),
|
||||
minActiveRelayPool: await getMinActiveRelayPoolFn(),
|
||||
});
|
||||
|
||||
const runQuery = async (relaySet, phase) => {
|
||||
const { prioritized } = prioritizeRelays(relaySet, healthConfig);
|
||||
const { prioritized } = healthManager.prioritizeRelays(relaySet, healthConfig);
|
||||
if (prioritized.length > 0) {
|
||||
errorLogger(`[${phase}] Querying ${prioritized.length} relays (${relayListLabel(prioritized)})...`);
|
||||
}
|
||||
@@ -331,14 +421,14 @@ export async function queryLocks(relays, cadence, dateStr, namespace, deps = {})
|
||||
);
|
||||
const elapsedMs = Date.now() - startedAtMs;
|
||||
for (const relay of prioritized) {
|
||||
recordRelayOutcome(relay, true, null, elapsedMs, healthConfig);
|
||||
healthManager.recordOutcome(relay, true, null, elapsedMs, healthConfig);
|
||||
}
|
||||
return filterActiveLocks(events.map(parseLockEvent));
|
||||
} catch (err) {
|
||||
const elapsedMs = Date.now() - startedAtMs;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
for (const relay of prioritized) {
|
||||
recordRelayOutcome(relay, false, message, elapsedMs, healthConfig);
|
||||
healthManager.recordOutcome(relay, false, message, elapsedMs, healthConfig);
|
||||
}
|
||||
throw new Error(
|
||||
`[${phase}] Relay query failed (timeout=${queryTimeoutMs}ms, relays=${relayListLabel(prioritized)}): ${message}`,
|
||||
@@ -350,12 +440,12 @@ export async function queryLocks(relays, cadence, dateStr, namespace, deps = {})
|
||||
const allRelays = mergeRelayList(relays, fallbackRelays);
|
||||
|
||||
try {
|
||||
maybeLogHealthSnapshot(allRelays, healthConfig, healthLogger, 'query:periodic');
|
||||
healthManager.maybeLogSnapshot(allRelays, healthConfig, healthLogger, 'query:periodic');
|
||||
try {
|
||||
return await runQuery(relays, 'query:primary');
|
||||
} catch (primaryErr) {
|
||||
if (!fallbackRelays.length) {
|
||||
maybeLogHealthSnapshot(allRelays, healthConfig, healthLogger, 'query:failure', true);
|
||||
healthManager.maybeLogSnapshot(allRelays, healthConfig, healthLogger, 'query:failure', true);
|
||||
throw primaryErr;
|
||||
}
|
||||
errorLogger(`WARN: ${primaryErr.message}`);
|
||||
@@ -398,65 +488,123 @@ async function publishToRelays(pool, relays, event, publishTimeoutMs, phase) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function publishLock(relays, event, deps = {}) {
|
||||
const {
|
||||
poolFactory = () => new SimplePool(),
|
||||
getPublishTimeoutMsFn = getPublishTimeoutMs,
|
||||
getMinSuccessfulRelayPublishesFn = getMinSuccessfulRelayPublishes,
|
||||
getRelayFallbacksFn = getRelayFallbacks,
|
||||
getMinActiveRelayPoolFn = getMinActiveRelayPool,
|
||||
retryAttempts = 4,
|
||||
retryBaseDelayMs = 500,
|
||||
retryCapDelayMs = 8_000,
|
||||
sleepFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
||||
randomFn = Math.random,
|
||||
telemetryLogger = console.error,
|
||||
healthLogger = console.error,
|
||||
diagnostics = {},
|
||||
} = deps;
|
||||
export class LockPublisher {
|
||||
constructor(relays, event, deps = {}) {
|
||||
this.relays = relays;
|
||||
this.event = event;
|
||||
this.deps = deps;
|
||||
this.pool = null;
|
||||
this.healthConfig = null;
|
||||
this.allRelays = [];
|
||||
this.fallbackRelays = [];
|
||||
this.publishTimeoutMs = 0;
|
||||
this.minSuccesses = 0;
|
||||
this.minActiveRelayPool = 0;
|
||||
this.maxAttempts = 0;
|
||||
this.retryBaseDelayMs = 0;
|
||||
this.retryCapDelayMs = 0;
|
||||
this.sleepFn = null;
|
||||
this.randomFn = null;
|
||||
this.telemetryLogger = null;
|
||||
this.healthLogger = null;
|
||||
this.healthManager = null;
|
||||
this.correlationId = null;
|
||||
this.attemptId = null;
|
||||
}
|
||||
|
||||
const correlationId = diagnostics.correlationId || process.env.SCHEDULER_LOCK_CORRELATION_ID || 'none';
|
||||
const attemptId = diagnostics.attemptId || process.env.SCHEDULER_LOCK_ATTEMPT_ID || '1';
|
||||
async publish() {
|
||||
const {
|
||||
poolFactory = () => new SimplePool(),
|
||||
getPublishTimeoutMsFn = getPublishTimeoutMs,
|
||||
getMinSuccessfulRelayPublishesFn = getMinSuccessfulRelayPublishes,
|
||||
getRelayFallbacksFn = getRelayFallbacks,
|
||||
getMinActiveRelayPoolFn = getMinActiveRelayPool,
|
||||
retryAttempts = 4,
|
||||
retryBaseDelayMs = 500,
|
||||
retryCapDelayMs = 8_000,
|
||||
sleepFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
||||
randomFn = Math.random,
|
||||
telemetryLogger = console.error,
|
||||
healthLogger = console.error,
|
||||
diagnostics = {},
|
||||
healthManager = defaultHealthManager,
|
||||
} = this.deps;
|
||||
|
||||
const pool = poolFactory();
|
||||
const publishTimeoutMs = getPublishTimeoutMsFn();
|
||||
const minSuccesses = getMinSuccessfulRelayPublishesFn();
|
||||
const fallbackRelays = getRelayFallbacksFn().filter((relay) => !relays.includes(relay));
|
||||
const maxAttempts = Math.max(1, Math.floor(retryAttempts));
|
||||
const healthConfig = buildRelayHealthConfig({
|
||||
...deps,
|
||||
minActiveRelayPool: getMinActiveRelayPoolFn(),
|
||||
});
|
||||
this.healthManager = healthManager;
|
||||
this.pool = poolFactory();
|
||||
this.publishTimeoutMs = this.deps.resolvedConfig?.publishTimeoutMs;
|
||||
this.minSuccesses = this.deps.resolvedConfig?.minSuccesses;
|
||||
this.fallbackRelays = (this.deps.resolvedConfig?.fallbackRelays || []).filter((relay) => !this.relays.includes(relay));
|
||||
this.maxAttempts = Math.max(1, Math.floor(retryAttempts));
|
||||
this.healthConfig = buildRelayHealthConfig({
|
||||
...this.deps,
|
||||
minActiveRelayPool: this.deps.resolvedConfig?.minActiveRelayPool,
|
||||
});
|
||||
|
||||
const publishOnce = async () => {
|
||||
this.retryBaseDelayMs = retryBaseDelayMs;
|
||||
this.retryCapDelayMs = retryCapDelayMs;
|
||||
this.sleepFn = sleepFn;
|
||||
this.randomFn = randomFn;
|
||||
this.telemetryLogger = telemetryLogger;
|
||||
this.healthLogger = healthLogger;
|
||||
this.healthManager = healthManager;
|
||||
this.correlationId = diagnostics.correlationId || randomUUID();
|
||||
this.attemptId = diagnostics.attemptId || randomUUID();
|
||||
|
||||
this.allRelays = mergeRelayList(this.relays, this.fallbackRelays);
|
||||
|
||||
try {
|
||||
this.healthManager.maybeLogSnapshot(this.allRelays, this.healthConfig, this.healthLogger, 'publish:periodic');
|
||||
let lastAttemptState = null;
|
||||
const retryTimeline = [];
|
||||
const overallStartedAt = Date.now();
|
||||
let terminalFailureCategory = PUBLISH_FAILURE_CATEGORIES.NON_RETRYABLE;
|
||||
|
||||
for (let attemptNumber = 1; attemptNumber <= this.maxAttempts; attemptNumber += 1) {
|
||||
const startedAtMs = Date.now();
|
||||
lastAttemptState = await this.executePublishCycle();
|
||||
const elapsedMs = Date.now() - startedAtMs;
|
||||
retryTimeline.push({
|
||||
publishAttempt: attemptNumber,
|
||||
successCount: lastAttemptState.successCount,
|
||||
relayAttemptedCount: lastAttemptState.attempted.size,
|
||||
elapsedMs,
|
||||
});
|
||||
|
||||
if (lastAttemptState.successCount >= this.minSuccesses) {
|
||||
this.logSuccess(attemptNumber, lastAttemptState, retryTimeline, overallStartedAt);
|
||||
return this.event;
|
||||
}
|
||||
|
||||
const { hasTransientFailure, hasPermanentFailure } = this.analyzeFailures(lastAttemptState.failures);
|
||||
const canRetry = attemptNumber < this.maxAttempts && hasTransientFailure && !hasPermanentFailure;
|
||||
|
||||
terminalFailureCategory = this.determineFailureCategory(hasTransientFailure, hasPermanentFailure, attemptNumber);
|
||||
|
||||
if (!canRetry) {
|
||||
break;
|
||||
}
|
||||
|
||||
const nextDelayMs = calculateBackoffDelayMs(attemptNumber, this.retryBaseDelayMs, this.retryCapDelayMs, this.randomFn);
|
||||
this.logRetry(attemptNumber, lastAttemptState.failures, elapsedMs, nextDelayMs);
|
||||
await this.sleepFn(nextDelayMs);
|
||||
}
|
||||
|
||||
this.handleFinalFailure(lastAttemptState, terminalFailureCategory, retryTimeline, overallStartedAt);
|
||||
} finally {
|
||||
this.pool.close(this.allRelays);
|
||||
}
|
||||
}
|
||||
|
||||
async executePublishCycle() {
|
||||
const attempted = new Set();
|
||||
const publishResults = [];
|
||||
|
||||
const attemptPhase = async (phaseRelays, phaseName) => {
|
||||
if (!phaseRelays.length) return;
|
||||
const { prioritized } = prioritizeRelays(phaseRelays, healthConfig);
|
||||
if (!prioritized.length) return;
|
||||
|
||||
console.error(`[${phaseName}] Publishing to ${prioritized.length} relays (${prioritized.join(', ')})...`);
|
||||
|
||||
const startedAtMs = Date.now();
|
||||
for (const relay of prioritized) {
|
||||
attempted.add(relay);
|
||||
}
|
||||
const phaseResults = await publishToRelays(pool, prioritized, event, publishTimeoutMs, phaseName);
|
||||
const elapsedMs = Date.now() - startedAtMs;
|
||||
for (const result of phaseResults) {
|
||||
result.latencyMs = elapsedMs;
|
||||
recordRelayOutcome(result.relay, result.success, result.message, elapsedMs, healthConfig);
|
||||
}
|
||||
publishResults.push(...phaseResults);
|
||||
};
|
||||
|
||||
await attemptPhase(relays, 'publish:primary');
|
||||
await this.attemptPhase(this.relays, 'publish:primary', attempted, publishResults);
|
||||
|
||||
let successCount = publishResults.filter((result) => result.success).length;
|
||||
if (successCount < minSuccesses && fallbackRelays.length > 0) {
|
||||
await attemptPhase(fallbackRelays, 'publish:fallback');
|
||||
if (successCount < this.minSuccesses && this.fallbackRelays.length > 0) {
|
||||
await this.attemptPhase(this.fallbackRelays, 'publish:fallback', attempted, publishResults);
|
||||
successCount = publishResults.filter((result) => result.success).length;
|
||||
}
|
||||
|
||||
@@ -468,89 +616,88 @@ export async function publishLock(relays, event, deps = {}) {
|
||||
}));
|
||||
|
||||
return { attempted, publishResults, successCount, failures };
|
||||
};
|
||||
}
|
||||
|
||||
const allRelays = mergeRelayList(relays, fallbackRelays);
|
||||
async attemptPhase(phaseRelays, phaseName, attempted, publishResults) {
|
||||
if (!phaseRelays.length) return;
|
||||
const { prioritized } = this.healthManager.prioritizeRelays(phaseRelays, this.healthConfig);
|
||||
if (!prioritized.length) return;
|
||||
|
||||
try {
|
||||
maybeLogHealthSnapshot(allRelays, healthConfig, healthLogger, 'publish:periodic');
|
||||
let lastAttemptState = null;
|
||||
const retryTimeline = [];
|
||||
const overallStartedAt = Date.now();
|
||||
let terminalFailureCategory = 'relay_publish_non_retryable';
|
||||
for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
|
||||
const startedAtMs = Date.now();
|
||||
lastAttemptState = await publishOnce();
|
||||
const elapsedMs = Date.now() - startedAtMs;
|
||||
retryTimeline.push({
|
||||
publishAttempt: attemptNumber,
|
||||
successCount: lastAttemptState.successCount,
|
||||
relayAttemptedCount: lastAttemptState.attempted.size,
|
||||
elapsedMs,
|
||||
});
|
||||
console.error(`[${phaseName}] Publishing to ${prioritized.length} relays (${prioritized.join(', ')})...`);
|
||||
|
||||
if (lastAttemptState.successCount >= minSuccesses) {
|
||||
telemetryLogger(JSON.stringify({
|
||||
event: 'lock_publish_quorum_met',
|
||||
correlationId,
|
||||
attemptId,
|
||||
publishAttempt: attemptNumber,
|
||||
successCount: lastAttemptState.successCount,
|
||||
relayAttemptedCount: lastAttemptState.attempted.size,
|
||||
requiredSuccesses: minSuccesses,
|
||||
timeoutMs: publishTimeoutMs,
|
||||
retryTimeline,
|
||||
totalElapsedMs: Date.now() - overallStartedAt,
|
||||
}));
|
||||
console.error(
|
||||
` Published to ${lastAttemptState.successCount}/${lastAttemptState.attempted.size} relays `
|
||||
+ `(required=${minSuccesses}, timeout=${publishTimeoutMs}ms)`,
|
||||
);
|
||||
return event;
|
||||
}
|
||||
|
||||
const hasTransientFailure = lastAttemptState.failures.some((failure) => isTransientPublishCategory(failure.category));
|
||||
const hasPermanentFailure = lastAttemptState.failures.some((failure) => !isTransientPublishCategory(failure.category));
|
||||
const canRetry = attemptNumber < maxAttempts && hasTransientFailure && !hasPermanentFailure;
|
||||
|
||||
if (hasTransientFailure && !hasPermanentFailure && attemptNumber >= maxAttempts) {
|
||||
terminalFailureCategory = 'relay_publish_quorum_failure';
|
||||
} else if (hasPermanentFailure) {
|
||||
terminalFailureCategory = 'relay_publish_non_retryable';
|
||||
} else {
|
||||
terminalFailureCategory = 'relay_publish_quorum_failure';
|
||||
}
|
||||
|
||||
if (!canRetry) {
|
||||
break;
|
||||
}
|
||||
|
||||
const nextDelayMs = calculateBackoffDelayMs(attemptNumber, retryBaseDelayMs, retryCapDelayMs, randomFn);
|
||||
for (const failure of lastAttemptState.failures) {
|
||||
if (!isTransientPublishCategory(failure.category)) continue;
|
||||
telemetryLogger(JSON.stringify({
|
||||
event: 'lock_publish_retry',
|
||||
correlationId,
|
||||
attemptId,
|
||||
publishAttempt: attemptNumber,
|
||||
relayUrl: failure.relay,
|
||||
errorCategory: failure.category,
|
||||
elapsedMs,
|
||||
nextDelayMs,
|
||||
}));
|
||||
}
|
||||
|
||||
await sleepFn(nextDelayMs);
|
||||
const startedAtMs = Date.now();
|
||||
for (const relay of prioritized) {
|
||||
attempted.add(relay);
|
||||
}
|
||||
const phaseResults = await publishToRelays(this.pool, prioritized, this.event, this.publishTimeoutMs, phaseName);
|
||||
const elapsedMs = Date.now() - startedAtMs;
|
||||
for (const result of phaseResults) {
|
||||
result.latencyMs = elapsedMs;
|
||||
this.healthManager.recordOutcome(result.relay, result.success, result.message, elapsedMs, this.healthConfig);
|
||||
}
|
||||
publishResults.push(...phaseResults);
|
||||
}
|
||||
|
||||
analyzeFailures(failures) {
|
||||
const hasTransientFailure = failures.some((failure) => isTransientPublishCategory(failure.category));
|
||||
const hasPermanentFailure = failures.some((failure) => !isTransientPublishCategory(failure.category));
|
||||
return { hasTransientFailure, hasPermanentFailure };
|
||||
}
|
||||
|
||||
determineFailureCategory(hasTransientFailure, hasPermanentFailure, attemptNumber) {
|
||||
if (hasTransientFailure && !hasPermanentFailure && attemptNumber >= this.maxAttempts) {
|
||||
return PUBLISH_FAILURE_CATEGORIES.QUORUM_FAILURE;
|
||||
} else if (hasPermanentFailure) {
|
||||
return PUBLISH_FAILURE_CATEGORIES.NON_RETRYABLE;
|
||||
} else {
|
||||
return PUBLISH_FAILURE_CATEGORIES.QUORUM_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
logSuccess(attemptNumber, lastAttemptState, retryTimeline, overallStartedAt) {
|
||||
this.telemetryLogger(JSON.stringify({
|
||||
event: 'lock_publish_quorum_met',
|
||||
correlationId: this.correlationId,
|
||||
attemptId: this.attemptId,
|
||||
publishAttempt: attemptNumber,
|
||||
successCount: lastAttemptState.successCount,
|
||||
relayAttemptedCount: lastAttemptState.attempted.size,
|
||||
requiredSuccesses: this.minSuccesses,
|
||||
timeoutMs: this.publishTimeoutMs,
|
||||
retryTimeline,
|
||||
totalElapsedMs: Date.now() - overallStartedAt,
|
||||
}));
|
||||
console.error(
|
||||
` Published to ${lastAttemptState.successCount}/${lastAttemptState.attempted.size} relays `
|
||||
+ `(required=${this.minSuccesses}, timeout=${this.publishTimeoutMs}ms)`,
|
||||
);
|
||||
}
|
||||
|
||||
logRetry(attemptNumber, failures, elapsedMs, nextDelayMs) {
|
||||
for (const failure of failures) {
|
||||
if (!isTransientPublishCategory(failure.category)) continue;
|
||||
this.telemetryLogger(JSON.stringify({
|
||||
event: 'lock_publish_retry',
|
||||
correlationId: this.correlationId,
|
||||
attemptId: this.attemptId,
|
||||
publishAttempt: attemptNumber,
|
||||
relayUrl: failure.relay,
|
||||
errorCategory: failure.category,
|
||||
elapsedMs,
|
||||
nextDelayMs,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleFinalFailure(lastAttemptState, terminalFailureCategory, retryTimeline, overallStartedAt) {
|
||||
const reasonDistribution = {};
|
||||
const failureLines = lastAttemptState.failures
|
||||
.map((result) => {
|
||||
reasonDistribution[result.category] = (reasonDistribution[result.category] || 0) + 1;
|
||||
telemetryLogger(JSON.stringify({
|
||||
this.telemetryLogger(JSON.stringify({
|
||||
event: 'lock_publish_failure',
|
||||
correlationId,
|
||||
attemptId,
|
||||
correlationId: this.correlationId,
|
||||
attemptId: this.attemptId,
|
||||
relayUrl: result.relay,
|
||||
phase: result.phase,
|
||||
reason: result.category,
|
||||
@@ -559,34 +706,59 @@ export async function publishLock(relays, event, deps = {}) {
|
||||
return `${result.relay} (${result.phase}, reason=${result.category}): ${result.message}`;
|
||||
});
|
||||
|
||||
telemetryLogger(JSON.stringify({
|
||||
this.telemetryLogger(JSON.stringify({
|
||||
event: 'lock_publish_quorum_failed',
|
||||
correlationId,
|
||||
attemptId,
|
||||
correlationId: this.correlationId,
|
||||
attemptId: this.attemptId,
|
||||
errorCategory: terminalFailureCategory,
|
||||
successCount: lastAttemptState.successCount,
|
||||
relayAttemptedCount: lastAttemptState.attempted.size,
|
||||
requiredSuccesses: minSuccesses,
|
||||
timeoutMs: publishTimeoutMs,
|
||||
attempts: maxAttempts,
|
||||
requiredSuccesses: this.minSuccesses,
|
||||
timeoutMs: this.publishTimeoutMs,
|
||||
attempts: this.maxAttempts,
|
||||
reasonDistribution,
|
||||
retryTimeline,
|
||||
totalElapsedMs: Date.now() - overallStartedAt,
|
||||
}));
|
||||
|
||||
maybeLogHealthSnapshot(allRelays, healthConfig, healthLogger, 'publish:failure', true);
|
||||
this.healthManager.maybeLogSnapshot(this.allRelays, this.healthConfig, this.healthLogger, 'publish:failure', true);
|
||||
throw new Error(
|
||||
`Failed relay publish quorum in publish phase: ${lastAttemptState.successCount}/${lastAttemptState.attempted.size} successful `
|
||||
+ `(required=${minSuccesses}, timeout=${publishTimeoutMs}ms, attempts=${maxAttempts}, attempt_id=${attemptId}, correlation_id=${correlationId}, error_category=${terminalFailureCategory}, total_retry_timeline_ms=${Date.now() - overallStartedAt})\n`
|
||||
+ `(required=${this.minSuccesses}, timeout=${this.publishTimeoutMs}ms, attempts=${this.maxAttempts}, attempt_id=${this.attemptId}, correlation_id=${this.correlationId}, error_category=${terminalFailureCategory}, total_retry_timeline_ms=${Date.now() - overallStartedAt})\n`
|
||||
+ ` retry timeline: ${retryTimeline.map((item) => `#${item.publishAttempt}:${item.elapsedMs}ms`).join(', ')}\n`
|
||||
+ ` ${failureLines.join('\n ')}`,
|
||||
);
|
||||
} finally {
|
||||
pool.close(allRelays);
|
||||
}
|
||||
}
|
||||
|
||||
export function _resetRelayHealthState() {
|
||||
relayHealthState.metricsByRelay.clear();
|
||||
relayHealthState.lastSnapshotAt = 0;
|
||||
export async function publishLock(relays, event, deps = {}) {
|
||||
const {
|
||||
getPublishTimeoutMsFn = getPublishTimeoutMs,
|
||||
getMinSuccessfulRelayPublishesFn = getMinSuccessfulRelayPublishes,
|
||||
getRelayFallbacksFn = getRelayFallbacks,
|
||||
getMinActiveRelayPoolFn = getMinActiveRelayPool,
|
||||
} = deps;
|
||||
|
||||
const [publishTimeoutMs, minSuccesses, fallbackRelays, minActiveRelayPool] = await Promise.all([
|
||||
getPublishTimeoutMsFn(),
|
||||
getMinSuccessfulRelayPublishesFn(),
|
||||
getRelayFallbacksFn(),
|
||||
getMinActiveRelayPoolFn(),
|
||||
]);
|
||||
|
||||
return new LockPublisher(relays, event, {
|
||||
...deps,
|
||||
resolvedConfig: {
|
||||
publishTimeoutMs,
|
||||
minSuccesses,
|
||||
fallbackRelays,
|
||||
minActiveRelayPool,
|
||||
},
|
||||
}).publish();
|
||||
}
|
||||
|
||||
export function _resetRelayHealthState() {
|
||||
defaultHealthManager.reset();
|
||||
}
|
||||
|
||||
export const _secureRandom = secureRandom;
|
||||
+31
-20
@@ -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 });
|
||||
@@ -152,36 +147,33 @@ async function interactiveInit(cwd) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers = null) {
|
||||
// Use interactive mode to get configuration unless mock answers are provided
|
||||
let config;
|
||||
async function resolveConfiguration(cwd, mockAnswers) {
|
||||
if (mockAnswers) {
|
||||
config = mockAnswers;
|
||||
} else {
|
||||
config = await interactiveInit(cwd);
|
||||
return mockAnswers;
|
||||
}
|
||||
const { installDir, namespace, relays } = config;
|
||||
return await interactiveInit(cwd);
|
||||
}
|
||||
|
||||
const paths = getPaths(cwd, installDir);
|
||||
console.log(`\nInitializing torch in ${paths.torchDir}...`);
|
||||
|
||||
// Check if directory exists and is not empty (unless force is used or we are installing to .)
|
||||
function ensureInstallDirectory(paths, force, installDir) {
|
||||
if (fs.existsSync(paths.torchDir) && !force) {
|
||||
const entries = fs.readdirSync(paths.torchDir);
|
||||
if (entries.length > 0 && installDir !== '.') {
|
||||
throw new Error(`Directory ${paths.torchDir} already exists and is not empty. Use --force to overwrite.`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureDir(paths.torchDir);
|
||||
ensureDir(paths.promptsDir);
|
||||
}
|
||||
|
||||
function installAppAssets(torchDir, installDir) {
|
||||
// 1. Copy App Directories
|
||||
syncAppDirectories(paths.torchDir, 'Copied');
|
||||
syncAppDirectories(torchDir, 'Copied');
|
||||
|
||||
// 2. Copy App Files
|
||||
syncAppFiles(paths.torchDir, installDir, 'Copied');
|
||||
syncAppFiles(torchDir, installDir, 'Copied');
|
||||
}
|
||||
|
||||
function installTorchAssets(paths, installDir) {
|
||||
// 3. Copy Roster
|
||||
const srcRoster = path.join(SRC_PROMPTS_DIR, 'roster.json');
|
||||
if (fs.existsSync(srcRoster)) {
|
||||
@@ -215,7 +207,9 @@ export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers =
|
||||
console.log(`Created ${files.length} files in ${path.relative(paths.root, destDir)}/`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function configureTorch(cwd, paths, installDir, namespace, relays) {
|
||||
// 6. Create/Update torch-config.json
|
||||
const configPath = path.join(paths.root, 'torch-config.json');
|
||||
|
||||
@@ -268,12 +262,29 @@ export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers =
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf8');
|
||||
console.log(`Saved configuration to ${path.relative(cwd, configPath)}`);
|
||||
}
|
||||
|
||||
function injectHostScriptsIfNeeded(paths, installDir) {
|
||||
// 7. Inject Scripts into Host Package.json
|
||||
// If we are NOT installing to '.', the host package.json is in paths.root
|
||||
if (installDir !== '.') {
|
||||
injectScriptsIntoHost(paths.root, installDir);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers = null) {
|
||||
const config = await resolveConfiguration(cwd, mockAnswers);
|
||||
const { installDir, namespace, relays } = config;
|
||||
|
||||
const paths = getPaths(cwd, installDir);
|
||||
console.log(`\nInitializing torch in ${paths.torchDir}...`);
|
||||
|
||||
ensureInstallDirectory(paths, force, installDir);
|
||||
|
||||
installAppAssets(paths.torchDir, installDir);
|
||||
installTorchAssets(paths, installDir);
|
||||
configureTorch(cwd, paths, installDir, namespace, relays);
|
||||
injectHostScriptsIfNeeded(paths, installDir);
|
||||
|
||||
console.log('\nInitialization complete.');
|
||||
console.log('You can now customize the files in ' + path.relative(cwd, paths.torchDir) + '/');
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { loadTorchConfig, _resetTorchConfigCache } from '../torch-config.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const configPath = path.resolve(process.cwd(), 'torch-config.json');
|
||||
const originalConfig = fs.existsSync(configPath) ? fs.readFileSync(configPath) : null;
|
||||
|
||||
// Ensure a config file exists for benchmark
|
||||
if (!fs.existsSync(configPath)) {
|
||||
fs.writeFileSync(configPath, JSON.stringify({
|
||||
nostrLock: {
|
||||
relays: ["wss://relay.damus.io"],
|
||||
namespace: "test-ns"
|
||||
}
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
const ITERATIONS = 1000;
|
||||
const start = process.hrtime.bigint();
|
||||
|
||||
async function run() {
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
_resetTorchConfigCache();
|
||||
try {
|
||||
const res = await loadTorchConfig();
|
||||
// Use res to prevent dead code elimination if JIT is smart (though side effects exist)
|
||||
if (!res) throw new Error("Failed");
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const end = process.hrtime.bigint();
|
||||
const duration = Number(end - start) / 1e6; // ms
|
||||
|
||||
console.log(`[ASYNC] Loaded config ${ITERATIONS} times in ${duration.toFixed(2)}ms`);
|
||||
console.log(`[ASYNC] Average: ${(duration / ITERATIONS).toFixed(4)}ms per load`);
|
||||
|
||||
// Cleanup
|
||||
if (!originalConfig && fs.existsSync(configPath)) {
|
||||
fs.unlinkSync(configPath);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
+12
-3
@@ -138,6 +138,8 @@ async function probePublishRead(relayUrl, namespace, timeoutMs) {
|
||||
try {
|
||||
message = JSON.parse(raw.toString());
|
||||
} catch {
|
||||
// Ignore malformed messages. The relay might be sending non-JSON data
|
||||
// or we might have a protocol mismatch. We only care about valid JSON.
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(message) || message.length < 2) return;
|
||||
@@ -194,15 +196,22 @@ async function appendHistory(historyPath, entry) {
|
||||
export async function runRelayHealthCheck(options = {}) {
|
||||
const {
|
||||
cadence = 'daily',
|
||||
relays = getRelays(),
|
||||
namespace = getNamespace(),
|
||||
timeoutMs = 6000,
|
||||
historyPath = path.resolve(process.cwd(), 'task-logs', 'relay-health', `${cadence}.jsonl`),
|
||||
allRelaysDownMinutes = 10,
|
||||
minSuccessRate = 0.7,
|
||||
windowMinutes = 60,
|
||||
} = options;
|
||||
|
||||
let {
|
||||
relays,
|
||||
namespace,
|
||||
historyPath,
|
||||
} = options;
|
||||
|
||||
if (!relays) relays = await getRelays();
|
||||
if (!namespace) namespace = await getNamespace();
|
||||
if (!historyPath) historyPath = path.resolve(process.cwd(), 'task-logs', 'relay-health', `${cadence}.jsonl`);
|
||||
|
||||
const relayResults = [];
|
||||
for (const relay of relays) {
|
||||
const relayResult = {
|
||||
|
||||
+17
-7
@@ -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 function getRoster(cadence) {
|
||||
const config = 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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -198,10 +197,15 @@ export async function ingestEvents(events, options = {}) {
|
||||
* @returns {Promise<import('./schema.js').MemoryRecord[]>} Sorted list of relevant memories.
|
||||
*/
|
||||
export async function getRelevantMemories(params) {
|
||||
const { repository = memoryRepository, ...queryParams } = params;
|
||||
const {
|
||||
repository = memoryRepository,
|
||||
ranker = filterAndRankMemories,
|
||||
cache: cacheProvider = cache,
|
||||
...queryParams
|
||||
} = params;
|
||||
const telemetry = buildTelemetryEmitter(params);
|
||||
const cacheKey = JSON.stringify(queryParams);
|
||||
const cached = cache.get(cacheKey);
|
||||
const cached = cacheProvider.get(cacheKey);
|
||||
if (cached) {
|
||||
telemetry('memory:retrieved', {
|
||||
agent_id: queryParams.agent_id,
|
||||
@@ -220,9 +224,9 @@ export async function getRelevantMemories(params) {
|
||||
? await repository.listMemories(queryParams)
|
||||
: [...memoryStore.values()];
|
||||
|
||||
const ranked = await filterAndRankMemories(source, queryParams);
|
||||
const ranked = await ranker(source, queryParams);
|
||||
await updateMemoryUsage(repository, ranked.map((memory) => memory.id));
|
||||
cache.set(cacheKey, ranked);
|
||||
cacheProvider.set(cacheKey, ranked);
|
||||
|
||||
telemetry('memory:retrieved', {
|
||||
agent_id: queryParams.agent_id,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,227 @@
|
||||
|
||||
> [email protected] test:unit:lock-backend
|
||||
> node --test test/lock-ops.test.mjs test/lock-ops-publish-retry.test.mjs
|
||||
|
||||
TAP version 13
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# Published to 1/1 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# Published to 1/1 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# (node:3973) ExperimentalWarning: The MockTimers API is an experimental feature and might change at any time
|
||||
# (Use `node --trace-warnings ...` to show where the warning was created)
|
||||
# Subtest: publishLock retry behavior
|
||||
# Subtest: succeeds immediately on first attempt without retries
|
||||
ok 1 - succeeds immediately on first attempt without retries
|
||||
---
|
||||
duration_ms: 6.654526
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: retries a transient failure then succeeds within retry budget with bounded jitter
|
||||
ok 2 - retries a transient failure then succeeds within retry budget with bounded jitter
|
||||
---
|
||||
duration_ms: 4.073858
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: returns terminal relay_publish_quorum_failure after exhausting retry budget
|
||||
ok 3 - returns terminal relay_publish_quorum_failure after exhausting retry budget
|
||||
---
|
||||
duration_ms: 5.742571
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: short-circuits non-retryable failures without extra attempts
|
||||
ok 4 - short-circuits non-retryable failures without extra attempts
|
||||
---
|
||||
duration_ms: 2.299061
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 1 - publishLock retry behavior
|
||||
---
|
||||
duration_ms: 20.363862
|
||||
type: 'suite'
|
||||
...
|
||||
# Subtest: lock-ops
|
||||
# Subtest: parseLockEvent
|
||||
# Subtest: parses a valid lock event correctly
|
||||
ok 1 - parses a valid lock event correctly
|
||||
---
|
||||
duration_ms: 1.874948
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
ok 1 - parseLockEvent
|
||||
---
|
||||
duration_ms: 2.822323
|
||||
type: 'suite'
|
||||
...
|
||||
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
|
||||
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
|
||||
# Published to 2/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-bad)...
|
||||
# [publish:primary] Publishing to 5 relays (relay-timeout, wss://dns, wss://tcp, wss://tls, wss://ws)...
|
||||
# [publish:primary] Publishing to 2 relays (wss://relay-bad, wss://relay-ok)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"7d70b395-2145-4470-9f33-9b602a34792d","attemptId":"c0035955-5f07-4d6b-a450-e07811f00e2f","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-bad)...
|
||||
# [publish:fallback] Publishing to 1 relays (wss://fallback-ok)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"239da9db-92a7-44f8-b694-52a4a6ddf437","attemptId":"12267c26-1926-4f5b-835b-f92682e5ffc4","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 2 relays (wss://bad, wss://healthy)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"54df96c1-356d-40a2-a300-0a5fce94d1f9","attemptId":"72e3fd52-0c75-4f3f-a43f-241ed8d973e1","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 2 relays (wss://healthy, wss://bad)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"35e05397-38ab-4fd5-b979-9f731dc5765a","attemptId":"c6410080-1244-4f4b-9139-dbb85187b441","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 2 relays (wss://healthy, wss://bad)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"e6f1916c-879a-4ed2-953e-7a519c11d857","attemptId":"1aa9c36c-8755-45ca-9fe6-6a89f883c152","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://healthy)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"56c8195a-74eb-4b72-a495-3e1efcb4a3f0","attemptId":"d2e0b655-7115-4f61-91fc-139dc5ec9db4","publishAttempt":1,"successCount":1,"relayAttemptedCount":1,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":1,"elapsedMs":0}],"totalElapsedMs":0}
|
||||
# Published to 1/1 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
|
||||
# [publish:fallback] Publishing to 1 relays (wss://fallback-healthy)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"94251c93-ebf1-481d-8fb5-c8a73bdf0a20","attemptId":"533fb740-98ec-4358-9d32-78c8b0ac72a4","publishAttempt":1,"successCount":1,"relayAttemptedCount":3,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":3,"elapsedMs":0}],"totalElapsedMs":0}
|
||||
# Published to 1/3 relays (required=1, timeout=200ms)
|
||||
# Subtest: queryLocks
|
||||
# Subtest: falls back to fallback relays when primary query fails
|
||||
ok 1 - falls back to fallback relays when primary query fails
|
||||
---
|
||||
duration_ms: 27.235965
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
ok 2 - queryLocks
|
||||
---
|
||||
duration_ms: 27.514651
|
||||
type: 'suite'
|
||||
...
|
||||
# Subtest: publishLock
|
||||
# Subtest: retries transient failures and succeeds on a later attempt
|
||||
not ok 1 - retries transient failures and succeeds on a later attempt
|
||||
---
|
||||
duration_ms: 6.53189
|
||||
type: 'test'
|
||||
location: '/app/test/lock-ops.test.mjs:78:5'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
{
|
||||
+ attemptId: '52c2baa6-8ff8-4f42-b6b3-ead606942315',
|
||||
+ correlationId: '6428aa2b-60f0-40a5-b385-525fc25c46b0',
|
||||
- attemptId: 'attempt-1',
|
||||
- correlationId: 'corr-1',
|
||||
elapsedMs: 1,
|
||||
errorCategory: 'connection_reset',
|
||||
event: 'lock_publish_retry',
|
||||
nextDelayMs: 250,
|
||||
publishAttempt: 1,
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
event: 'lock_publish_retry'
|
||||
correlationId: 'corr-1'
|
||||
attemptId: 'attempt-1'
|
||||
publishAttempt: 1
|
||||
relayUrl: 'wss://primary-a'
|
||||
errorCategory: 'connection_reset'
|
||||
elapsedMs: 1
|
||||
nextDelayMs: 250
|
||||
actual:
|
||||
event: 'lock_publish_retry'
|
||||
correlationId: '6428aa2b-60f0-40a5-b385-525fc25c46b0'
|
||||
attemptId: '52c2baa6-8ff8-4f42-b6b3-ead606942315'
|
||||
publishAttempt: 1
|
||||
relayUrl: 'wss://primary-a'
|
||||
errorCategory: 'connection_reset'
|
||||
elapsedMs: 1
|
||||
nextDelayMs: 250
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///app/test/lock-ops.test.mjs:124:14)
|
||||
async Test.run (node:internal/test_runner/test:1054:7)
|
||||
async Promise.all (index 0)
|
||||
async Suite.run (node:internal/test_runner/test:1442:7)
|
||||
async Suite.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||
...
|
||||
# Subtest: fails after retry budget is exhausted for persistent transient failures
|
||||
ok 2 - fails after retry budget is exhausted for persistent transient failures
|
||||
---
|
||||
duration_ms: 2.875565
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: fails without retry for persistent validation failures
|
||||
ok 3 - fails without retry for persistent validation failures
|
||||
---
|
||||
duration_ms: 1.360655
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: includes per-relay reason categories and retry metadata in failure diagnostics
|
||||
ok 4 - includes per-relay reason categories and retry metadata in failure diagnostics
|
||||
---
|
||||
duration_ms: 2.145677
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: returns success when mixed relay outcomes still satisfy quorum
|
||||
ok 5 - returns success when mixed relay outcomes still satisfy quorum
|
||||
---
|
||||
duration_ms: 1.586006
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: uses fallback relays to satisfy min success quorum
|
||||
ok 6 - uses fallback relays to satisfy min success quorum
|
||||
---
|
||||
duration_ms: 1.059862
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: quarantines repeatedly failing relays and still reaches quorum with one healthy relay
|
||||
ok 7 - quarantines repeatedly failing relays and still reaches quorum with one healthy relay
|
||||
---
|
||||
duration_ms: 2.500909
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: uses fallback and min active pool to reintroduce quarantined relay when needed
|
||||
ok 8 - uses fallback and min active pool to reintroduce quarantined relay when needed
|
||||
---
|
||||
duration_ms: 1.073546
|
||||
type: 'test'
|
||||
...
|
||||
1..8
|
||||
not ok 3 - publishLock
|
||||
---
|
||||
duration_ms: 19.967883
|
||||
type: 'suite'
|
||||
location: '/app/test/lock-ops.test.mjs:77:3'
|
||||
failureType: 'subtestsFailed'
|
||||
error: '1 subtest failed'
|
||||
code: 'ERR_TEST_FAILURE'
|
||||
...
|
||||
1..3
|
||||
not ok 2 - lock-ops
|
||||
---
|
||||
duration_ms: 52.981447
|
||||
type: 'suite'
|
||||
location: '/app/test/lock-ops.test.mjs:10:1'
|
||||
failureType: 'subtestsFailed'
|
||||
error: '1 subtest failed'
|
||||
code: 'ERR_TEST_FAILURE'
|
||||
...
|
||||
1..2
|
||||
# tests 14
|
||||
# suites 5
|
||||
# pass 13
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 196.37029
|
||||
@@ -0,0 +1,175 @@
|
||||
|
||||
> [email protected] test:unit:lock-backend
|
||||
> node --test test/lock-ops.test.mjs test/lock-ops-publish-retry.test.mjs
|
||||
|
||||
TAP version 13
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# Published to 1/1 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# Published to 1/1 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
|
||||
# (node:5876) ExperimentalWarning: The MockTimers API is an experimental feature and might change at any time
|
||||
# (Use `node --trace-warnings ...` to show where the warning was created)
|
||||
# Subtest: publishLock retry behavior
|
||||
# Subtest: succeeds immediately on first attempt without retries
|
||||
ok 1 - succeeds immediately on first attempt without retries
|
||||
---
|
||||
duration_ms: 6.413745
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: retries a transient failure then succeeds within retry budget with bounded jitter
|
||||
ok 2 - retries a transient failure then succeeds within retry budget with bounded jitter
|
||||
---
|
||||
duration_ms: 3.062285
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: returns terminal relay_publish_quorum_failure after exhausting retry budget
|
||||
ok 3 - returns terminal relay_publish_quorum_failure after exhausting retry budget
|
||||
---
|
||||
duration_ms: 4.611267
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: short-circuits non-retryable failures without extra attempts
|
||||
ok 4 - short-circuits non-retryable failures without extra attempts
|
||||
---
|
||||
duration_ms: 2.07494
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 1 - publishLock retry behavior
|
||||
---
|
||||
duration_ms: 17.852297
|
||||
type: 'suite'
|
||||
...
|
||||
# Subtest: lock-ops
|
||||
# Subtest: parseLockEvent
|
||||
# Subtest: parses a valid lock event correctly
|
||||
ok 1 - parses a valid lock event correctly
|
||||
---
|
||||
duration_ms: 1.583449
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
ok 1 - parseLockEvent
|
||||
---
|
||||
duration_ms: 2.601962
|
||||
type: 'suite'
|
||||
...
|
||||
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
|
||||
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
|
||||
# Published to 2/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-bad)...
|
||||
# [publish:primary] Publishing to 5 relays (relay-timeout, wss://dns, wss://tcp, wss://tls, wss://ws)...
|
||||
# [publish:primary] Publishing to 2 relays (wss://relay-bad, wss://relay-ok)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"1be06dfa-a31e-42b0-b789-327fd9e11131","attemptId":"10405dd5-2f84-4e23-9a5a-696abb34c1bc","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://primary-bad)...
|
||||
# [publish:fallback] Publishing to 1 relays (wss://fallback-ok)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"becb5a52-cef6-4bc6-84b1-ca4d14d63e04","attemptId":"25dca8be-61ee-42b8-a764-7ebdc46f234b","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 2 relays (wss://bad, wss://healthy)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"67ecc056-eaa3-40c2-9042-15e033b7a252","attemptId":"07181123-946d-4823-bc4a-c79d13a84920","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 2 relays (wss://healthy, wss://bad)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"6075ee96-8def-4111-84e2-d7b395950a84","attemptId":"bb03ae8b-ff47-41e2-ba91-b3031d38c497","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 2 relays (wss://healthy, wss://bad)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"38f8db94-b61b-4b02-9a33-05fd8da73e5e","attemptId":"e645bbac-64ec-4f61-a865-a91d0adc739c","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
|
||||
# Published to 1/2 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 1 relays (wss://healthy)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"4c6a0906-bd5b-431b-9af1-1d4a87aa59d3","attemptId":"13836194-8337-49af-8f81-8038a7101d4b","publishAttempt":1,"successCount":1,"relayAttemptedCount":1,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":1,"elapsedMs":0}],"totalElapsedMs":0}
|
||||
# Published to 1/1 relays (required=1, timeout=200ms)
|
||||
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
|
||||
# [publish:fallback] Publishing to 1 relays (wss://fallback-healthy)...
|
||||
# {"event":"lock_publish_quorum_met","correlationId":"2e7e8c9f-5b93-4cd3-8324-a63b981e9e4c","attemptId":"d53e657e-431a-4203-a017-256ba03a05db","publishAttempt":1,"successCount":1,"relayAttemptedCount":3,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":3,"elapsedMs":1}],"totalElapsedMs":2}
|
||||
# Published to 1/3 relays (required=1, timeout=200ms)
|
||||
# Subtest: queryLocks
|
||||
# Subtest: falls back to fallback relays when primary query fails
|
||||
ok 1 - falls back to fallback relays when primary query fails
|
||||
---
|
||||
duration_ms: 27.905124
|
||||
type: 'test'
|
||||
...
|
||||
1..1
|
||||
ok 2 - queryLocks
|
||||
---
|
||||
duration_ms: 28.20218
|
||||
type: 'suite'
|
||||
...
|
||||
# Subtest: publishLock
|
||||
# Subtest: retries transient failures and succeeds on a later attempt
|
||||
ok 1 - retries transient failures and succeeds on a later attempt
|
||||
---
|
||||
duration_ms: 4.944259
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: fails after retry budget is exhausted for persistent transient failures
|
||||
ok 2 - fails after retry budget is exhausted for persistent transient failures
|
||||
---
|
||||
duration_ms: 3.551748
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: fails without retry for persistent validation failures
|
||||
ok 3 - fails without retry for persistent validation failures
|
||||
---
|
||||
duration_ms: 1.406267
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: includes per-relay reason categories and retry metadata in failure diagnostics
|
||||
ok 4 - includes per-relay reason categories and retry metadata in failure diagnostics
|
||||
---
|
||||
duration_ms: 2.101245
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: returns success when mixed relay outcomes still satisfy quorum
|
||||
ok 5 - returns success when mixed relay outcomes still satisfy quorum
|
||||
---
|
||||
duration_ms: 1.547853
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: uses fallback relays to satisfy min success quorum
|
||||
ok 6 - uses fallback relays to satisfy min success quorum
|
||||
---
|
||||
duration_ms: 1.380374
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: quarantines repeatedly failing relays and still reaches quorum with one healthy relay
|
||||
ok 7 - quarantines repeatedly failing relays and still reaches quorum with one healthy relay
|
||||
---
|
||||
duration_ms: 3.040514
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: uses fallback and min active pool to reintroduce quarantined relay when needed
|
||||
ok 8 - uses fallback and min active pool to reintroduce quarantined relay when needed
|
||||
---
|
||||
duration_ms: 2.862114
|
||||
type: 'test'
|
||||
...
|
||||
1..8
|
||||
ok 3 - publishLock
|
||||
---
|
||||
duration_ms: 21.884946
|
||||
type: 'suite'
|
||||
...
|
||||
1..3
|
||||
ok 2 - lock-ops
|
||||
---
|
||||
duration_ms: 53.519877
|
||||
type: 'suite'
|
||||
...
|
||||
1..2
|
||||
# tests 14
|
||||
# suites 5
|
||||
# pass 14
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 212.244831
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
agent: content-audit-agent
|
||||
cadence: daily
|
||||
run-start: 2026-02-17T21:29:25Z
|
||||
prompt-path: src/prompts/daily/content-audit-agent.md
|
||||
---
|
||||
|
||||
# Test Log
|
||||
|
||||
- **Command**: `ls -F /content`
|
||||
- **Result**: `ls: cannot access '/content': No such file or directory` (Anticipated)
|
||||
|
||||
- **Command**: `npm run lint`
|
||||
- **Result**: Pending execution
|
||||
@@ -0,0 +1,202 @@
|
||||
# Test Log: Decompose scripts/agent/run-scheduler-cycle.mjs
|
||||
|
||||
## 1. File Size Report
|
||||
Before: 1127 lines
|
||||
After: 788 lines
|
||||
|
||||
## 2. Linting
|
||||
Command: `npm run lint`
|
||||
Output: No errors in modified files.
|
||||
|
||||
## 3. Unit/Integration Tests
|
||||
Command: `node --test test/scheduler-lock-failure-schema.contract.test.mjs test/run-scheduler-cycle-memory-policy.test.mjs test/scheduler-preflight-lock.e2e.test.mjs test/scheduler-ratchet.test.mjs`
|
||||
|
||||
### Output
|
||||
```
|
||||
TAP version 13
|
||||
# Subtest: fails required memory policy when retrieval/storage evidence is missing
|
||||
ok 1 - fails required memory policy when retrieval/storage evidence is missing
|
||||
---
|
||||
duration_ms: 757.539101
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: accepts required memory policy when markers or artifacts are produced
|
||||
ok 2 - accepts required memory policy when markers or artifacts are produced
|
||||
---
|
||||
duration_ms: 762.353236
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: records backend failure metadata when lock command exits with code 2
|
||||
ok 3 - records backend failure metadata when lock command exits with code 2
|
||||
---
|
||||
duration_ms: 907.42582
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: defers backend failures in non-strict mode and reuses idempotency key on successful retry
|
||||
ok 4 - defers backend failures in non-strict mode and reuses idempotency key on successful retry
|
||||
---
|
||||
duration_ms: 515.738251
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: fails after exceeding non-strict deferral budget
|
||||
ok 5 - fails after exceeding non-strict deferral budget
|
||||
---
|
||||
duration_ms: 169.270959
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: strict lock mode fails immediately without deferral
|
||||
ok 6 - strict lock mode fails immediately without deferral
|
||||
---
|
||||
duration_ms: 84.263333
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: skips lock health preflight by default when not enabled
|
||||
ok 7 - skips lock health preflight by default when not enabled
|
||||
---
|
||||
duration_ms: 351.440095
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: fails scheduler with preflight metadata when lock health preflight is enabled
|
||||
ok 8 - fails scheduler with preflight metadata when lock health preflight is enabled
|
||||
---
|
||||
duration_ms: 122.484853
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: categorizes unreadable prompt files as prompt_parse_error
|
||||
ok 9 - categorizes unreadable prompt files as prompt_parse_error
|
||||
---
|
||||
duration_ms: 87.191858
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: categorizes invalid prompt schema as prompt_schema_error
|
||||
ok 10 - categorizes invalid prompt schema as prompt_schema_error
|
||||
---
|
||||
duration_ms: 87.797569
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: categorizes handoff runtime failures as execution_error
|
||||
ok 11 - categorizes handoff runtime failures as execution_error
|
||||
---
|
||||
duration_ms: 324.626141
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: retries lock acquisition for backend error and succeeds on a later attempt
|
||||
ok 12 - retries lock acquisition for backend error and succeeds on a later attempt
|
||||
---
|
||||
duration_ms: 367.965729
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: stops retrying after configured lock backend retries are exhausted
|
||||
ok 13 - stops retrying after configured lock backend retries are exhausted
|
||||
---
|
||||
duration_ms: 93.241513
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: does not use backend retry flow for exit code 3 lock conflicts
|
||||
ok 14 - does not use backend retry flow for exit code 3 lock conflicts
|
||||
---
|
||||
duration_ms: 364.90421
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: scheduler lock backend failure artifact matches required frontmatter schema
|
||||
ok 15 - scheduler lock backend failure artifact matches required frontmatter schema
|
||||
---
|
||||
duration_ms: 99.881359
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: schema contract catches missing required lock-backend field
|
||||
ok 16 - schema contract catches missing required lock-backend field
|
||||
---
|
||||
duration_ms: 1.852952
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: schema contract catches misnamed lock-backend key
|
||||
ok 17 - schema contract catches misnamed lock-backend key
|
||||
---
|
||||
duration_ms: 0.591693
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: lock preflight e2e: successful lock writes completed status snapshot
|
||||
ok 18 - lock preflight e2e: successful lock writes completed status snapshot
|
||||
---
|
||||
duration_ms: 375.694252
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: lock preflight e2e: exit code 2 quorum failure persists failed backend status and prompt-not-started marker
|
||||
ok 19 - lock preflight e2e: exit code 2 quorum failure persists failed backend status and prompt-not-started marker
|
||||
---
|
||||
duration_ms: 88.901459
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: lock preflight e2e: non-lock failure exits failed without prompt parse/schema classification
|
||||
ok 20 - lock preflight e2e: non-lock failure exits failed without prompt parse/schema classification
|
||||
---
|
||||
duration_ms: 84.551615
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: Scheduler Ratchet Logic (Log Checking)
|
||||
# Subtest: excludes agents completed today (daily)
|
||||
ok 1 - excludes agents completed today (daily)
|
||||
---
|
||||
duration_ms: 2.674164
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: excludes agents completed this week (weekly)
|
||||
ok 2 - excludes agents completed this week (weekly)
|
||||
---
|
||||
duration_ms: 0.457543
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: combines locked and completed agents in exclusion list
|
||||
ok 3 - combines locked and completed agents in exclusion list
|
||||
---
|
||||
duration_ms: 0.529591
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: ignores logs when --ignore-logs is set
|
||||
ok 4 - ignores logs when --ignore-logs is set
|
||||
---
|
||||
duration_ms: 0.486543
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: handles missing log directory gracefully
|
||||
ok 5 - handles missing log directory gracefully
|
||||
---
|
||||
duration_ms: 0.365618
|
||||
type: 'test'
|
||||
...
|
||||
1..5
|
||||
ok 21 - Scheduler Ratchet Logic (Log Checking)
|
||||
---
|
||||
duration_ms: 5.944697
|
||||
type: 'suite'
|
||||
...
|
||||
# Subtest: Scheduler cycle ordering guarantees
|
||||
# Subtest: keeps optional preflight before lock-acquire and preserves handoff/artifact/validation/complete ordering
|
||||
ok 1 - keeps optional preflight before lock-acquire and preserves handoff/artifact/validation/complete ordering
|
||||
---
|
||||
duration_ms: 11.091114
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: includes lock backend failure metadata and classifier checkpoints
|
||||
ok 2 - includes lock backend failure metadata and classifier checkpoints
|
||||
---
|
||||
duration_ms: 2.007641
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
ok 22 - Scheduler cycle ordering guarantees
|
||||
---
|
||||
duration_ms: 13.942915
|
||||
type: 'suite'
|
||||
...
|
||||
1..22
|
||||
# tests 27
|
||||
# suites 2
|
||||
# pass 27
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5077.329481
|
||||
```
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
# Pending Tasks
|
||||
- [ ] Run `npm test` to identify flakes.
|
||||
- [ ] Fix identified flakes.
|
||||
- [ ] Document findings.
|
||||
|
||||
- agent: ci-health-agent
|
||||
- cadence: daily
|
||||
- session: 2026-02-17T19-36-32Z
|
||||
- run-start: 2026-02-17T19-36-32Z
|
||||
- prompt-path: src/prompts/daily/ci-health-agent.md
|
||||
@@ -0,0 +1 @@
|
||||
- [x] Extract lock-ops constants to src/constants.mjs
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
agent: content-audit-agent
|
||||
cadence: daily
|
||||
run-start: 2026-02-17T21:29:25Z
|
||||
prompt-path: src/prompts/daily/content-audit-agent.md
|
||||
---
|
||||
|
||||
# Audit Tasks
|
||||
|
||||
## Completed Tasks
|
||||
- [x] Check for existence of `/content` directory
|
||||
- [x] If exists, inventory claims (Skipped - missing content)
|
||||
- [x] If missing, report finding and exit (Done)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Todo: Decompose scripts/agent/run-scheduler-cycle.mjs
|
||||
|
||||
- [ ] Extract `scripts/agent/scheduler-utils.mjs`
|
||||
- [ ] `runCommand`
|
||||
- [ ] `getSafeEnv`
|
||||
- [ ] `sleep`
|
||||
- [ ] `parseJsonFromOutput`
|
||||
- [ ] `parseJsonEventsFromOutput`
|
||||
- [ ] `redactSensitive`
|
||||
- [ ] `excerptText`
|
||||
- [ ] `parseNonNegativeInt`
|
||||
- [ ] `parseBooleanFlag`
|
||||
- [ ] `readJson`
|
||||
- [ ] `normalizeStringList`
|
||||
- [ ] Extract `scripts/agent/scheduler-lock.mjs`
|
||||
- [ ] `acquireLockWithRetry`
|
||||
- [ ] `summarizeLockFailureReasons`
|
||||
- [ ] `classifyLockBackendError`
|
||||
- [ ] `buildLockBackendRemediation`
|
||||
- [ ] `runLockHealthPreflight`
|
||||
@@ -0,0 +1,11 @@
|
||||
# Tasks
|
||||
|
||||
- Agent: deps-security-agent
|
||||
- Cadence: daily
|
||||
- Run Start: 2026-02-17T23-13-04Z
|
||||
- Prompt: src/prompts/daily/deps-security-agent.md
|
||||
|
||||
## Completed Tasks
|
||||
- [x] Document `nostr-tools` update availability in report.
|
||||
- [x] Document `eslint` vulnerabilities in report.
|
||||
- [x] Create `artifacts/deps-report.md`.
|
||||
@@ -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`.
|
||||
+77
-80
@@ -21,9 +21,9 @@ function parsePositiveInteger(value) {
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
function parseRelayList(value) {
|
||||
function parseStringList(value) {
|
||||
if (!Array.isArray(value)) return null;
|
||||
const parsed = value.map((relay) => String(relay).trim()).filter(Boolean);
|
||||
const parsed = value.map((item) => String(item).trim()).filter(Boolean);
|
||||
return parsed.length > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
@@ -63,11 +63,6 @@ function normalizeStatus(value, fallback) {
|
||||
return normalized === 'active' || normalized === 'all' ? normalized : fallback;
|
||||
}
|
||||
|
||||
function parseRoster(value) {
|
||||
if (!Array.isArray(value)) return null;
|
||||
const parsed = value.map((item) => String(item).trim()).filter(Boolean);
|
||||
return parsed.length > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
export function getTorchConfigPath() {
|
||||
const explicitPath = (process.env.TORCH_CONFIG_PATH || '').trim();
|
||||
@@ -94,10 +89,8 @@ export function parseTorchConfig(raw, configPath = null) {
|
||||
raw,
|
||||
nostrLock: {
|
||||
namespace: typeof nostrLock.namespace === 'string' ? nostrLock.namespace.trim() : null,
|
||||
relays: Array.isArray(nostrLock.relays)
|
||||
? nostrLock.relays.map((relay) => String(relay).trim()).filter(Boolean)
|
||||
: null,
|
||||
relayFallbacks: parseRelayList(nostrLock.relayFallbacks),
|
||||
relays: parseStringList(nostrLock.relays),
|
||||
relayFallbacks: parseStringList(nostrLock.relayFallbacks),
|
||||
ttlSeconds: Number.isFinite(nostrLock.ttlSeconds) && nostrLock.ttlSeconds > 0
|
||||
? Math.floor(nostrLock.ttlSeconds)
|
||||
: null,
|
||||
@@ -105,15 +98,13 @@ export function parseTorchConfig(raw, configPath = null) {
|
||||
publishTimeoutMs: parsePositiveInteger(nostrLock.publishTimeoutMs),
|
||||
minSuccessfulRelayPublishes: parsePositiveInteger(nostrLock.minSuccessfulRelayPublishes),
|
||||
minActiveRelayPool: parsePositiveInteger(nostrLock.minActiveRelayPool),
|
||||
dailyRoster: parseRoster(nostrLock.dailyRoster),
|
||||
weeklyRoster: parseRoster(nostrLock.weeklyRoster),
|
||||
dailyRoster: parseStringList(nostrLock.dailyRoster),
|
||||
weeklyRoster: parseStringList(nostrLock.weeklyRoster),
|
||||
},
|
||||
dashboard: {
|
||||
defaultCadenceView: normalizeCadence(dashboard.defaultCadenceView, 'daily'),
|
||||
defaultStatusView: normalizeStatus(dashboard.defaultStatusView, 'active'),
|
||||
relays: Array.isArray(dashboard.relays)
|
||||
? dashboard.relays.map((relay) => String(relay).trim()).filter(Boolean)
|
||||
: null,
|
||||
relays: parseStringList(dashboard.relays),
|
||||
namespace: typeof dashboard.namespace === 'string' ? dashboard.namespace.trim() : null,
|
||||
hashtag: typeof dashboard.hashtag === 'string' ? dashboard.hashtag.trim() : null,
|
||||
auth: typeof dashboard.auth === 'string' ? dashboard.auth.trim() : null,
|
||||
@@ -124,8 +115,8 @@ export function parseTorchConfig(raw, configPath = null) {
|
||||
weekly: typeof firstPromptByCadence.weekly === 'string' ? firstPromptByCadence.weekly.trim() : null,
|
||||
},
|
||||
paused: {
|
||||
daily: parseRoster(paused.daily) || [],
|
||||
weekly: parseRoster(paused.weekly) || [],
|
||||
daily: parseStringList(paused.daily) || [],
|
||||
weekly: parseStringList(paused.weekly) || [],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -136,18 +127,20 @@ export function _resetTorchConfigCache() {
|
||||
cachedConfig = null;
|
||||
}
|
||||
|
||||
export function loadTorchConfig(fileSystem = fs) {
|
||||
export async function loadTorchConfig(fileSystem = fs) {
|
||||
if (cachedConfig) return cachedConfig;
|
||||
|
||||
const configPath = getTorchConfigPath();
|
||||
let raw = {};
|
||||
|
||||
if (fileSystem.existsSync(configPath)) {
|
||||
try {
|
||||
raw = JSON.parse(fileSystem.readFileSync(configPath, 'utf8'));
|
||||
} catch (err) {
|
||||
try {
|
||||
const content = await fileSystem.promises.readFile(configPath, 'utf8');
|
||||
raw = JSON.parse(content);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw new Error(`Failed to parse ${configPath}: ${err.message}`, { cause: err });
|
||||
}
|
||||
// If file missing (ENOENT), we use empty object defaults
|
||||
}
|
||||
|
||||
cachedConfig = parseTorchConfig(raw, configPath);
|
||||
@@ -196,8 +189,20 @@ function parseEnvInteger(envValue, envName) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function getRelays() {
|
||||
const config = loadTorchConfig();
|
||||
function resolveIntegerConfig(envKey, configValue, defaultValue, validator, configLabel) {
|
||||
const envValue = process.env[envKey];
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, envKey);
|
||||
validator(parsed, envKey);
|
||||
return parsed;
|
||||
}
|
||||
const value = configValue || defaultValue;
|
||||
validator(value, `effective ${configLabel}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function getRelays() {
|
||||
const config = await loadTorchConfig();
|
||||
const envRelays = process.env.NOSTR_LOCK_RELAYS;
|
||||
if (envRelays) {
|
||||
return parseEnvRelayList(envRelays, 'NOSTR_LOCK_RELAYS');
|
||||
@@ -211,8 +216,8 @@ export function getRelays() {
|
||||
return DEFAULT_RELAYS;
|
||||
}
|
||||
|
||||
export function getRelayFallbacks() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getRelayFallbacks() {
|
||||
const config = await loadTorchConfig();
|
||||
const envRelays = process.env.NOSTR_LOCK_RELAY_FALLBACKS;
|
||||
if (envRelays) {
|
||||
return parseEnvRelayList(envRelays, 'NOSTR_LOCK_RELAY_FALLBACKS');
|
||||
@@ -220,14 +225,14 @@ export function getRelayFallbacks() {
|
||||
return config.nostrLock.relayFallbacks || [];
|
||||
}
|
||||
|
||||
export function getNamespace() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getNamespace() {
|
||||
const config = await loadTorchConfig();
|
||||
const namespace = (process.env.NOSTR_LOCK_NAMESPACE || config.nostrLock.namespace || DEFAULT_NAMESPACE).trim();
|
||||
return namespace || DEFAULT_NAMESPACE;
|
||||
}
|
||||
|
||||
export function getTtl() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getTtl() {
|
||||
const config = await loadTorchConfig();
|
||||
const envTtl = process.env.NOSTR_LOCK_TTL;
|
||||
if (envTtl) {
|
||||
const parsed = parseInt(envTtl, 10);
|
||||
@@ -239,60 +244,52 @@ export function getTtl() {
|
||||
return DEFAULT_TTL;
|
||||
}
|
||||
|
||||
export function getQueryTimeoutMs() {
|
||||
const config = loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_QUERY_TIMEOUT_MS;
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, 'NOSTR_LOCK_QUERY_TIMEOUT_MS');
|
||||
assertTimeoutInRange(parsed, 'NOSTR_LOCK_QUERY_TIMEOUT_MS');
|
||||
return parsed;
|
||||
}
|
||||
const value = config.nostrLock.queryTimeoutMs || DEFAULT_QUERY_TIMEOUT_MS;
|
||||
assertTimeoutInRange(value, 'effective query timeout');
|
||||
return value;
|
||||
export async function getQueryTimeoutMs() {
|
||||
const config = await loadTorchConfig();
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_QUERY_TIMEOUT_MS',
|
||||
config.nostrLock.queryTimeoutMs,
|
||||
DEFAULT_QUERY_TIMEOUT_MS,
|
||||
assertTimeoutInRange,
|
||||
'query timeout',
|
||||
);
|
||||
}
|
||||
|
||||
export function getPublishTimeoutMs() {
|
||||
const config = loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_PUBLISH_TIMEOUT_MS;
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, 'NOSTR_LOCK_PUBLISH_TIMEOUT_MS');
|
||||
assertTimeoutInRange(parsed, 'NOSTR_LOCK_PUBLISH_TIMEOUT_MS');
|
||||
return parsed;
|
||||
}
|
||||
const value = config.nostrLock.publishTimeoutMs || DEFAULT_PUBLISH_TIMEOUT_MS;
|
||||
assertTimeoutInRange(value, 'effective publish timeout');
|
||||
return value;
|
||||
export async function getPublishTimeoutMs() {
|
||||
const config = await loadTorchConfig();
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_PUBLISH_TIMEOUT_MS',
|
||||
config.nostrLock.publishTimeoutMs,
|
||||
DEFAULT_PUBLISH_TIMEOUT_MS,
|
||||
assertTimeoutInRange,
|
||||
'publish timeout',
|
||||
);
|
||||
}
|
||||
|
||||
export function getMinSuccessfulRelayPublishes() {
|
||||
const config = loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES;
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, 'NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES');
|
||||
assertPositiveCount(parsed, 'NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES');
|
||||
return parsed;
|
||||
}
|
||||
const value = config.nostrLock.minSuccessfulRelayPublishes || DEFAULT_MIN_SUCCESSFUL_PUBLISHES;
|
||||
assertPositiveCount(value, 'effective min successful relay publishes');
|
||||
return value;
|
||||
export async function getMinSuccessfulRelayPublishes() {
|
||||
const config = await loadTorchConfig();
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES',
|
||||
config.nostrLock.minSuccessfulRelayPublishes,
|
||||
DEFAULT_MIN_SUCCESSFUL_PUBLISHES,
|
||||
assertPositiveCount,
|
||||
'min successful relay publishes',
|
||||
);
|
||||
}
|
||||
|
||||
export function getMinActiveRelayPool() {
|
||||
const config = loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL;
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, 'NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL');
|
||||
assertPositiveCount(parsed, 'NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL');
|
||||
return parsed;
|
||||
}
|
||||
const value = config.nostrLock.minActiveRelayPool || DEFAULT_MIN_ACTIVE_RELAY_POOL;
|
||||
assertPositiveCount(value, 'effective min active relay pool');
|
||||
return value;
|
||||
export async function getMinActiveRelayPool() {
|
||||
const config = await loadTorchConfig();
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL',
|
||||
config.nostrLock.minActiveRelayPool,
|
||||
DEFAULT_MIN_ACTIVE_RELAY_POOL,
|
||||
assertPositiveCount,
|
||||
'min active relay pool',
|
||||
);
|
||||
}
|
||||
|
||||
export function getHashtag() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getHashtag() {
|
||||
const config = await loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_HASHTAG;
|
||||
if (envValue) {
|
||||
return envValue.trim();
|
||||
@@ -300,12 +297,12 @@ export function getHashtag() {
|
||||
if (config.dashboard.hashtag) {
|
||||
return config.dashboard.hashtag;
|
||||
}
|
||||
const namespace = getNamespace();
|
||||
const namespace = await getNamespace();
|
||||
return `${namespace}-agent-lock`;
|
||||
}
|
||||
|
||||
export function getDashboardAuth() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getDashboardAuth() {
|
||||
const config = await loadTorchConfig();
|
||||
const envValue = process.env.TORCH_DASHBOARD_AUTH;
|
||||
if (envValue) {
|
||||
return envValue.trim();
|
||||
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user