@ouro.bot/cli 0.1.0-alpha.966 → 0.1.0-alpha.968
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 +13 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/context-loss-sentinel.js +45 -0
- package/dist/heart/orientation-frame.js +15 -2
- package/dist/senses/shared-turn.js +24 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
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.968",
|
|
6
|
+
"changes": [
|
|
7
|
+
"The context-loss Sentinel takes over its write lock when the process that left it is gone. A lock left by a crash or a reboot used to block every Sentinel refresh until someone deleted it, which turned daemon health critical, so `ouro up` failed and self-update rolled new releases back."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.967",
|
|
12
|
+
"changes": [
|
|
13
|
+
"When you reply to an older Telegram message, the Butler now sees which message you are answering, so a quick \"yes\" goes to the question you meant instead of the newest one.",
|
|
14
|
+
"Replies to the Butler's control cards and notices are not mistaken for answers to his questions."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.1.0-alpha.966",
|
|
6
19
|
"changes": [
|
|
@@ -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.968</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -92,6 +92,49 @@ function atomicWriteJson(filePath, value) {
|
|
|
92
92
|
function sleep(ms) {
|
|
93
93
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
94
94
|
}
|
|
95
|
+
function processGone(pid) {
|
|
96
|
+
try {
|
|
97
|
+
process.kill(pid, 0);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return error.code === "ESRCH";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A lock whose writer died (a crash, a reboot) would otherwise block every refresh forever. It is taken over only when it
|
|
106
|
+
* names a process that no longer exists; any other content waits as before. The lock is renamed aside first, so of two
|
|
107
|
+
* waiters only one takes it, and a lock that changed owner in the meantime is put back.
|
|
108
|
+
*/
|
|
109
|
+
function breakDeadOwnerLock(lockPath) {
|
|
110
|
+
let owner;
|
|
111
|
+
try {
|
|
112
|
+
owner = fs.readFileSync(lockPath, "utf-8").trim();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (!/^\d+$/.test(owner) || !processGone(Number(owner)))
|
|
118
|
+
return false;
|
|
119
|
+
const aside = `${lockPath}.${process.pid}.${Date.now()}.stale`;
|
|
120
|
+
try {
|
|
121
|
+
fs.renameSync(lockPath, aside);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (fs.readFileSync(aside, "utf-8").trim() !== owner) {
|
|
127
|
+
try {
|
|
128
|
+
fs.linkSync(aside, lockPath);
|
|
129
|
+
}
|
|
130
|
+
catch { /* a newer lock exists; it owns the path */ }
|
|
131
|
+
fs.rmSync(aside, { force: true });
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
fs.rmSync(aside, { force: true });
|
|
135
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "heart", event: "heart.context_loss_sentinel_stale_lock", message: "took over a context-loss Sentinel lock whose owner process is gone", meta: { owner: Number(owner) } });
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
95
138
|
async function withFileLock(lockPath, timeoutMs, fn) {
|
|
96
139
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
97
140
|
const startedAt = Date.now();
|
|
@@ -112,6 +155,8 @@ async function withFileLock(lockPath, timeoutMs, fn) {
|
|
|
112
155
|
if (code !== "EEXIST") {
|
|
113
156
|
throw error;
|
|
114
157
|
}
|
|
158
|
+
if (breakDeadOwnerLock(lockPath))
|
|
159
|
+
continue;
|
|
115
160
|
if (Date.now() - startedAt > timeoutMs) {
|
|
116
161
|
throw new Error(`context-loss Sentinel lock timed out: ${lockPath}`);
|
|
117
162
|
}
|
|
@@ -5,6 +5,7 @@ exports.priorWorkInstruction = priorWorkInstruction;
|
|
|
5
5
|
exports.labelPriorWorkSurface = labelPriorWorkSurface;
|
|
6
6
|
exports.extractMessageText = extractMessageText;
|
|
7
7
|
exports.extractOrderedListReferents = extractOrderedListReferents;
|
|
8
|
+
exports.stampReplyingToText = stampReplyingToText;
|
|
8
9
|
exports.buildOrientationFrame = buildOrientationFrame;
|
|
9
10
|
exports.renderOrientationFrame = renderOrientationFrame;
|
|
10
11
|
const runtime_1 = require("../nerves/runtime");
|
|
@@ -188,6 +189,16 @@ function deriveActionPolicy(signals, combinedSpeech, priorAssistantReferents, la
|
|
|
188
189
|
clearedBy: CORRECTION_HOLD_CLEARED_BY,
|
|
189
190
|
};
|
|
190
191
|
}
|
|
192
|
+
// A reply names the message it answers. The sense stamps that message's text on the
|
|
193
|
+
// user message, so "yes" binds to what was replied to, not to the latest thing said.
|
|
194
|
+
function stampReplyingToText(message, text) {
|
|
195
|
+
;
|
|
196
|
+
message._replyingToText = text;
|
|
197
|
+
}
|
|
198
|
+
function replyingToTextOf(message) {
|
|
199
|
+
const text = message._replyingToText;
|
|
200
|
+
return typeof text === "string" && text.trim() ? text.trim() : undefined;
|
|
201
|
+
}
|
|
191
202
|
function buildOrientationFrame(input) {
|
|
192
203
|
const derivedCurrent = input.currentUserMessages
|
|
193
204
|
? { messages: input.currentUserMessages, firstIndex: input.messages.length }
|
|
@@ -195,8 +206,10 @@ function buildOrientationFrame(input) {
|
|
|
195
206
|
const currentUserSpeech = derivedCurrent.messages
|
|
196
207
|
.map((message) => extractMessageText(message))
|
|
197
208
|
.filter((text) => text.length > 0);
|
|
209
|
+
const replyingToText = derivedCurrent.messages.map(replyingToTextOf).find((text) => text !== undefined);
|
|
198
210
|
const previousAssistant = latestAssistantBefore(input.messages, derivedCurrent.firstIndex);
|
|
199
|
-
const priorAssistantReferents = extractOrderedListReferents(extractMessageText(previousAssistant));
|
|
211
|
+
const priorAssistantReferents = extractOrderedListReferents(replyingToText ?? extractMessageText(previousAssistant));
|
|
212
|
+
const source = replyingToText && !input.source?.replyingToText ? { kind: input.channel, ...input.source, replyingToText } : input.source;
|
|
200
213
|
const candidateStructuredOutput = latestStructuredOutputFrom(input.structuredOutputs);
|
|
201
214
|
const speechKind = input.speechKind ?? "utterance";
|
|
202
215
|
const signals = deriveSignals(currentUserSpeech, priorAssistantReferents, candidateStructuredOutput, speechKind);
|
|
@@ -213,7 +226,7 @@ function buildOrientationFrame(input) {
|
|
|
213
226
|
...(latestStructuredOutput ? { latestStructuredOutput } : {}),
|
|
214
227
|
signals,
|
|
215
228
|
actionPolicy,
|
|
216
|
-
...(
|
|
229
|
+
...(source ? { source } : {}),
|
|
217
230
|
...(speechKind === "utterance" ? {} : { speechKind }),
|
|
218
231
|
};
|
|
219
232
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -62,6 +62,7 @@ const pipeline_1 = require("./pipeline");
|
|
|
62
62
|
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
63
63
|
const runtime_1 = require("../nerves/runtime");
|
|
64
64
|
const session_transaction_1 = require("../mind/session-transaction");
|
|
65
|
+
const orientation_frame_1 = require("../heart/orientation-frame");
|
|
65
66
|
const turn_execution_lease_1 = require("../heart/turn-execution-lease");
|
|
66
67
|
const RESPONSE_CAP = 50_000;
|
|
67
68
|
const OUTWARD_DELIVERY_TOOL_ACKS = new Map([
|
|
@@ -270,6 +271,23 @@ function rawSessionEvents(value) {
|
|
|
270
271
|
const record = value;
|
|
271
272
|
return record.version === 2 && Array.isArray(record.events) ? record.events : [];
|
|
272
273
|
}
|
|
274
|
+
// What the agent said in a replied-to message: the settle answer it delivered, else its text.
|
|
275
|
+
// Only the agent's own messages are quoted; a control card or notice is not something it asked.
|
|
276
|
+
function spokenText(event) {
|
|
277
|
+
if (event?.role !== "assistant")
|
|
278
|
+
return "";
|
|
279
|
+
for (const call of event.toolCalls) {
|
|
280
|
+
if (call.function.name !== "settle")
|
|
281
|
+
continue;
|
|
282
|
+
try {
|
|
283
|
+
const answer = JSON.parse(call.function.arguments).answer;
|
|
284
|
+
if (typeof answer === "string" && answer.trim())
|
|
285
|
+
return answer.trim().slice(0, 1200);
|
|
286
|
+
}
|
|
287
|
+
catch { /* a malformed settle names nothing */ }
|
|
288
|
+
}
|
|
289
|
+
return typeof event.content === "string" ? event.content.trim().slice(0, 1200) : "";
|
|
290
|
+
}
|
|
273
291
|
function exactProjectedIngressMessage(existing, messages, eventId, nativeValue) {
|
|
274
292
|
if (!nativeValue || typeof nativeValue !== "object" || Array.isArray(nativeValue))
|
|
275
293
|
return null;
|
|
@@ -462,6 +480,8 @@ async function runSenseTurnExclusive(options, owner) {
|
|
|
462
480
|
system.content = system.content.split(`\n${marker}`)[0] + `\n${marker}\n${JSON.stringify(options.shepherdSession)}\nUse this exact UUID for shepherd_read/shepherd_send. If gone, fail explicitly; never substitute a short ref. This locator grants no authority.`;
|
|
463
481
|
}
|
|
464
482
|
}
|
|
483
|
+
const replyToEventId = precommittedIngressEvent?.relations.replyToEventId ?? options.ingressRelations?.replyToEventId;
|
|
484
|
+
const repliedToText = spokenText(existing?.events?.find((event) => event.id === replyToEventId));
|
|
465
485
|
if (precommittedIngressEvent) {
|
|
466
486
|
const projectedIngress = exactProjectedIngressMessage(existing, sessionMessages, precommittedIngressEvent.id, baseSession.value);
|
|
467
487
|
if (!projectedIngress || projectedIngress.role !== "user" || projectedIngress.content !== userMessage)
|
|
@@ -471,6 +491,8 @@ async function runSenseTurnExclusive(options, owner) {
|
|
|
471
491
|
threadRootEventId: precommittedIngressEvent.relations.threadRootEventId,
|
|
472
492
|
references: precommittedIngressEvent.relations.references,
|
|
473
493
|
});
|
|
494
|
+
if (repliedToText)
|
|
495
|
+
(0, orientation_frame_1.stampReplyingToText)(projectedIngress, repliedToText);
|
|
474
496
|
}
|
|
475
497
|
// Pending dir
|
|
476
498
|
const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
|
|
@@ -580,6 +602,8 @@ async function runSenseTurnExclusive(options, owner) {
|
|
|
580
602
|
(0, session_events_1.stampIngressTime)(userMsg);
|
|
581
603
|
if (options.ingressRelations)
|
|
582
604
|
(0, session_events_1.stampIngressRelations)(userMsg, options.ingressRelations);
|
|
605
|
+
if (repliedToText)
|
|
606
|
+
(0, orientation_frame_1.stampReplyingToText)(userMsg, repliedToText);
|
|
583
607
|
inboundMessages.push(userMsg);
|
|
584
608
|
}
|
|
585
609
|
const turnResult = await (0, pipeline_1.handleInboundTurn)({
|
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.968",
|
|
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.968",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|