@ouro.bot/cli 0.1.0-alpha.826 → 0.1.0-alpha.828
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +12 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +52 -37
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/awaiting/await-runtime-state.js +11 -0
- package/dist/heart/daemon/daemon-entry.js +10 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.828",
|
|
6
|
+
"changes": [
|
|
7
|
+
"The media MCP's deterministic core (download-state and diagnosis) is now a tested, importable seam instead of only being verifiable in production."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.827",
|
|
12
|
+
"changes": [
|
|
13
|
+
"Awaits advance their own cadence on dispatch, so a filed watch no longer fires forever without recording a check."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.826",
|
|
6
18
|
"changes": [
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Protocol: JSON-RPC 2.0 over stdio, newline framed, MCP 2024-11-05.
|
|
11
11
|
|
|
12
12
|
import { readFileSync } from "node:fs"
|
|
13
|
+
import { pathToFileURL } from "node:url"
|
|
13
14
|
|
|
14
15
|
const CRED_PATH = process.env.SANCTUARY_MEDIA_CREDENTIALS ?? "/home/ouro/AgentBundles/sanctuary.ouro/mcp/media-credentials.json"
|
|
15
16
|
|
|
@@ -425,31 +426,43 @@ async function mediaRequestStatus(a) {
|
|
|
425
426
|
last_grab_title: grabs[0]?.sourceTitle ?? null,
|
|
426
427
|
monitored: entity ? Boolean(entity.monitored) : null,
|
|
427
428
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
429
|
+
result.download = computeDownloadState(queueRecs, Date.now())
|
|
430
|
+
|
|
431
|
+
const chain = await chainHealth()
|
|
432
|
+
result.chain = chain
|
|
433
|
+
result.diagnosis = diagnose({ shelf, result, chain, entity, kind })
|
|
434
|
+
return result
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// The deterministic core, split out so it can be tested without a live
|
|
438
|
+
// Sonarr/Radarr. `computeDownloadState` and `diagnose` are the whole reason the
|
|
439
|
+
// media surface can answer "why hasn't this downloaded" without the agent
|
|
440
|
+
// guessing, and D-013 (a stalled download read as progress) shipped verified
|
|
441
|
+
// only in production because there was no seam to test them through.
|
|
442
|
+
//
|
|
443
|
+
// A season pack shows up as one queue row per episode, all sharing one
|
|
444
|
+
// downloadId. Summing rows would report a 45 GB pack as 450 GB, so size and
|
|
445
|
+
// "how many downloads" are counted per distinct download, not per row. A
|
|
446
|
+
// torrent that has not moved a byte since it was grabbed is dead, not slow:
|
|
447
|
+
// Radarr reports trackedDownloadStatus "ok" for these indefinitely, so
|
|
448
|
+
// progress is measured against age rather than trusted from the row.
|
|
449
|
+
export function computeDownloadState(queueRecs, nowMs) {
|
|
431
450
|
const byDownload = new Map()
|
|
432
451
|
for (const r of queueRecs) byDownload.set(r.downloadId ?? `row:${r.id}`, r)
|
|
433
452
|
const downloads = [...byDownload.values()]
|
|
434
453
|
const sizeLeft = downloads.reduce((s, r) => s + (r.sizeleft ?? 0), 0)
|
|
435
454
|
const sizeTotal = downloads.reduce((s, r) => s + (r.size ?? 0), 0)
|
|
436
|
-
// A torrent that has not moved a byte since it was grabbed is not slow, it is
|
|
437
|
-
// dead: no seeders, or a release the client cannot fetch. Radarr goes on
|
|
438
|
-
// reporting trackedDownloadStatus "ok" for these indefinitely, so a queue row
|
|
439
|
-
// on its own reads as healthy and any answer built from it reassures instead
|
|
440
|
-
// of acting. Measuring progress against age is what separates the two.
|
|
441
|
-
const now = Date.now()
|
|
442
455
|
const stalledItems = downloads
|
|
443
456
|
.filter((r) => (r.size ?? 0) > 0 && (r.sizeleft ?? 0) >= (r.size ?? 0) && r.added
|
|
444
|
-
&& (
|
|
457
|
+
&& (nowMs - Date.parse(r.added)) / 3_600_000 >= STALL_AFTER_HOURS)
|
|
445
458
|
.map((r) => ({
|
|
446
459
|
title: r.title ?? null,
|
|
447
460
|
added_at: r.added ?? null,
|
|
448
|
-
age_hours: Math.round((
|
|
461
|
+
age_hours: Math.round((nowMs - Date.parse(r.added)) / 3_600_000),
|
|
449
462
|
size_gb: Number(((r.size ?? 0) / 1073741824).toFixed(2)),
|
|
450
463
|
queue_id: r.id ?? null,
|
|
451
464
|
}))
|
|
452
|
-
|
|
465
|
+
return {
|
|
453
466
|
active_downloads: downloads.length,
|
|
454
467
|
active_items: queueRecs.length,
|
|
455
468
|
states: [...new Set(downloads.map((r) => r.status))],
|
|
@@ -461,15 +474,10 @@ async function mediaRequestStatus(a) {
|
|
|
461
474
|
stalled: stalledItems.length > 0,
|
|
462
475
|
stalled_items: stalledItems,
|
|
463
476
|
}
|
|
464
|
-
|
|
465
|
-
const chain = await chainHealth()
|
|
466
|
-
result.chain = chain
|
|
467
|
-
result.diagnosis = diagnose({ shelf, result, chain, entity, kind })
|
|
468
|
-
return result
|
|
469
477
|
}
|
|
470
478
|
|
|
471
479
|
// The deterministic core. The agent must never have to work this out itself.
|
|
472
|
-
function diagnose({ shelf, result, chain, entity, kind }) {
|
|
480
|
+
export function diagnose({ shelf, result, chain, entity, kind }) {
|
|
473
481
|
const file = result.file ?? {}
|
|
474
482
|
const onShelf = kind === "series" ? (file.episodes_on_disk ?? 0) > 0 : Boolean(file.on_shelf)
|
|
475
483
|
const complete = kind === "series" ? (file.episodes_on_disk ?? 0) >= (file.episodes_total ?? Infinity) : onShelf
|
|
@@ -759,22 +767,29 @@ function exitWhenIdle() {
|
|
|
759
767
|
if (stdinClosed && inFlight === 0) process.exit(0)
|
|
760
768
|
}
|
|
761
769
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
.
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
770
|
+
// Only run the stdio server when invoked directly (node media-mcp.mjs, exactly
|
|
771
|
+
// how agent.json launches it). Guarding this lets the test suite import the
|
|
772
|
+
// module for computeDownloadState/diagnose without attaching to stdin or exiting
|
|
773
|
+
// the test process. The launch is a plain node <abs-path>, so argv[1] is the
|
|
774
|
+
// file's own path and this comparison holds.
|
|
775
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
776
|
+
process.stdin.setEncoding("utf8")
|
|
777
|
+
process.stdin.on("data", (chunk) => {
|
|
778
|
+
buf += chunk
|
|
779
|
+
let nl
|
|
780
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
781
|
+
const line = buf.slice(0, nl).trim()
|
|
782
|
+
buf = buf.slice(nl + 1)
|
|
783
|
+
if (!line) continue
|
|
784
|
+
let msg
|
|
785
|
+
try { msg = JSON.parse(line) } catch { continue }
|
|
786
|
+
inFlight += 1
|
|
787
|
+
handle(msg)
|
|
788
|
+
.catch((e) => {
|
|
789
|
+
if (msg.id !== undefined) send({ jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: e.message } })
|
|
790
|
+
})
|
|
791
|
+
.finally(() => { inFlight -= 1; exitWhenIdle() })
|
|
792
|
+
}
|
|
793
|
+
})
|
|
794
|
+
process.stdin.on("end", () => { stdinClosed = true; exitWhenIdle() })
|
|
795
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.828</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.readAwaitRuntimeState = readAwaitRuntimeState;
|
|
37
37
|
exports.applyAwaitRuntimeState = applyAwaitRuntimeState;
|
|
38
38
|
exports.writeAwaitRuntimeState = writeAwaitRuntimeState;
|
|
39
|
+
exports.recordAwaitDispatch = recordAwaitDispatch;
|
|
39
40
|
exports.recordAwaitCheck = recordAwaitCheck;
|
|
40
41
|
const path = __importStar(require("path"));
|
|
41
42
|
const json_store_1 = require("../../arc/json-store");
|
|
@@ -89,6 +90,16 @@ function writeAwaitRuntimeState(agentRoot, name, partial) {
|
|
|
89
90
|
meta: { agentRoot, name, last_checked: merged.last_checked, checked_count: merged.checked_count },
|
|
90
91
|
});
|
|
91
92
|
}
|
|
93
|
+
// The scheduler owns cadence, not the model. `recordAwaitCheck` is written only
|
|
94
|
+
// when a woken turn calls `resolve_await` with verdict "no", so an await whose
|
|
95
|
+
// turn answers in chat, errors, or simply never calls the tool leaves
|
|
96
|
+
// `last_checked` null forever: it reads as "never checked" on every
|
|
97
|
+
// reconciliation, re-fires, wakes the private runtime again and records
|
|
98
|
+
// nothing. Recording the dispatch itself keeps the cadence honest whatever the
|
|
99
|
+
// turn does; the observation from `resolve_await` still layers on top.
|
|
100
|
+
function recordAwaitDispatch(agentRoot, name, now) {
|
|
101
|
+
writeAwaitRuntimeState(agentRoot, name, { last_checked: now });
|
|
102
|
+
}
|
|
92
103
|
function recordAwaitCheck(agentRoot, name, observation, now) {
|
|
93
104
|
const existing = readAwaitRuntimeState(agentRoot, name);
|
|
94
105
|
const nextCount = (existing?.checked_count ?? 0) + 1;
|
|
@@ -55,6 +55,7 @@ const habit_scheduler_1 = require("../habits/habit-scheduler");
|
|
|
55
55
|
const habit_migration_1 = require("../habits/habit-migration");
|
|
56
56
|
const await_scheduler_1 = require("../awaiting/await-scheduler");
|
|
57
57
|
const await_expiry_1 = require("../awaiting/await-expiry");
|
|
58
|
+
const await_runtime_state_1 = require("../awaiting/await-runtime-state");
|
|
58
59
|
const os_cron_deps_1 = require("./os-cron-deps");
|
|
59
60
|
const os_cron_1 = require("./os-cron");
|
|
60
61
|
const container_runtime_1 = require("./container-runtime");
|
|
@@ -801,7 +802,15 @@ void (0, daemon_bootstrap_startup_1.startDaemonAfterContainerCredentialBootstrap
|
|
|
801
802
|
agent,
|
|
802
803
|
awaitName,
|
|
803
804
|
triggerSource: "await-scheduler",
|
|
804
|
-
})).
|
|
805
|
+
})).then(() => {
|
|
806
|
+
// Record the dispatch here so the cadence advances whatever the woken
|
|
807
|
+
// turn does. Without this the only writer of `last_checked` is the
|
|
808
|
+
// agent calling `resolve_await`, so a turn that does anything else
|
|
809
|
+
// leaves the await permanently "never checked" and it re-fires on
|
|
810
|
+
// every reconciliation. Recorded only on a successful wake: a failed
|
|
811
|
+
// dispatch is not a check.
|
|
812
|
+
(0, await_runtime_state_1.recordAwaitDispatch)(bundleRoot, awaitName, new Date().toISOString());
|
|
813
|
+
}).catch((error) => {
|
|
805
814
|
emitAwaitPrivateWakeDispatchError({
|
|
806
815
|
agent,
|
|
807
816
|
awaitName,
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.828",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.828",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|