@ouro.bot/cli 0.1.0-alpha.951 → 0.1.0-alpha.953
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 +14 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/mind/friend-merge.js +43 -37
- package/dist/mind/outbound-record.js +5 -2
- package/dist/repertoire/tools-shepherd.js +1 -1
- package/dist/senses/bluebubbles/index.js +11 -13
- package/dist/senses/shepherd/owner-notice.js +6 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
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.953",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Shepherd works both ways on any channel: every notice ends with \"Reply here and I'll pass your answer on.\", and when you answer one in iMessage, Teams or any other conversation, your agent recognizes the notice as its own, reads that coding session in Herdr or cmux, passes your answer on, and tells you what it sent.",
|
|
8
|
+
"iMessage no longer drops a slow reply: a turn now has ten minutes instead of two before it is treated as hung, and a newer message from you still replaces an older turn. A turn that truly hangs can now mean up to ten minutes of silence, so send another message to move on. The health check calls a turn stalled only after five minutes."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.952",
|
|
13
|
+
"changes": [
|
|
14
|
+
"An iMessage held back by the recovery budget or storm breaker is no longer lost: it stays pending, and a later recovery pass handles it once the block lifts. Budget and breaker windows now run on the real clock instead of the message's own timestamp, so the block can actually lift.",
|
|
15
|
+
"`ouro friend dedupe` now finds records that share the same person identity, such as a stale stranger record holding an iMessage handle that a family record also owns, and proposes merging them into the higher-trust record. Group-chat handles are not treated as a shared identity."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
4
18
|
{
|
|
5
19
|
"version": "0.1.0-alpha.951",
|
|
6
20
|
"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.953</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -41,7 +41,7 @@ exports.dedupeFriends = dedupeFriends;
|
|
|
41
41
|
// Merging friend records into one person, and retiring the merged-away records.
|
|
42
42
|
//
|
|
43
43
|
// Two product paths use this: redeeming an identity link code (a new channel identity
|
|
44
|
-
// joins its owner's record) and deduplicating records that name the same
|
|
44
|
+
// joins its owner's record) and deduplicating records that name the same person.
|
|
45
45
|
// The target record stays authoritative: a merge never raises trust, admission,
|
|
46
46
|
// initiative or capability, so merging cannot be used to escalate anyone.
|
|
47
47
|
//
|
|
@@ -246,7 +246,11 @@ async function mergeFriends(input) {
|
|
|
246
246
|
function isLocalOnly(record) {
|
|
247
247
|
return record.externalIds.length > 0 && record.externalIds.every((externalId) => externalId.provider === "local");
|
|
248
248
|
}
|
|
249
|
+
const TRUST_RANK = { stranger: 0, acquaintance: 1, friend: 2, family: 3 };
|
|
249
250
|
function canonicalOrder(a, b) {
|
|
251
|
+
const trust = TRUST_RANK[b.trustLevel ?? "friend"] - TRUST_RANK[a.trustLevel ?? "friend"];
|
|
252
|
+
if (trust !== 0)
|
|
253
|
+
return trust;
|
|
250
254
|
const notes = Object.keys(b.notes).length - Object.keys(a.notes).length;
|
|
251
255
|
if (notes !== 0)
|
|
252
256
|
return notes;
|
|
@@ -254,46 +258,48 @@ function canonicalOrder(a, b) {
|
|
|
254
258
|
return a.createdAt < b.createdAt ? -1 : 1;
|
|
255
259
|
return a.id < b.id ? -1 : 1;
|
|
256
260
|
}
|
|
261
|
+
/** A group-chat handle names every member of the chat, so it never identifies one person. */
|
|
262
|
+
function isPersonIdentity(externalId) {
|
|
263
|
+
return externalId.provider !== "local"
|
|
264
|
+
&& !(externalId.provider === "imessage-handle" && externalId.externalId.startsWith("group:"));
|
|
265
|
+
}
|
|
257
266
|
/**
|
|
258
|
-
*
|
|
259
|
-
* same name and trust level, and are joined
|
|
260
|
-
*
|
|
267
|
+
* Human records are duplicates when either
|
|
268
|
+
* - they carry only local identities, share the same name and trust level, and are joined
|
|
269
|
+
* through at least one shared local identity, or
|
|
270
|
+
* - they share an identical person identity (provider, id and tenant). The friends store
|
|
271
|
+
* refuses to resolve such an identity because it is bound to more than one record.
|
|
272
|
+
* The canonical record is the one with the highest trust, then the most notes, then the oldest.
|
|
261
273
|
*/
|
|
262
274
|
function planFriendDedupe(records) {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
for (const bucket of buckets.values()) {
|
|
272
|
-
// Union records that share any local identity.
|
|
273
|
-
const parent = bucket.map((_, index) => index);
|
|
274
|
-
const find = (index) => (parent[index] === index ? index : (parent[index] = find(parent[index])));
|
|
275
|
-
const owner = new Map();
|
|
276
|
-
bucket.forEach((record, index) => {
|
|
277
|
-
for (const externalId of record.externalIds) {
|
|
278
|
-
const key = externalIdKey(externalId);
|
|
279
|
-
const seen = owner.get(key);
|
|
280
|
-
if (seen === undefined)
|
|
281
|
-
owner.set(key, index);
|
|
282
|
-
else
|
|
283
|
-
parent[find(index)] = find(seen);
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
const components = new Map();
|
|
287
|
-
bucket.forEach((record, index) => {
|
|
288
|
-
const root = find(index);
|
|
289
|
-
components.set(root, [...(components.get(root) ?? []), record]);
|
|
290
|
-
});
|
|
291
|
-
for (const members of components.values()) {
|
|
292
|
-
if (members.length < 2)
|
|
275
|
+
const humans = records.filter((record) => (record.kind ?? "human") === "human");
|
|
276
|
+
const parent = humans.map((_, index) => index);
|
|
277
|
+
const find = (index) => (parent[index] === index ? index : (parent[index] = find(parent[index])));
|
|
278
|
+
const owner = new Map();
|
|
279
|
+
humans.forEach((record, index) => {
|
|
280
|
+
const localBucket = isLocalOnly(record) ? [record.name.trim().toLowerCase(), record.trustLevel ?? "friend"] : null;
|
|
281
|
+
for (const externalId of record.externalIds) {
|
|
282
|
+
if (!localBucket && !isPersonIdentity(externalId))
|
|
293
283
|
continue;
|
|
294
|
-
const [
|
|
295
|
-
|
|
284
|
+
const key = JSON.stringify([...(localBucket ?? []), externalIdKey(externalId)]);
|
|
285
|
+
const seen = owner.get(key);
|
|
286
|
+
if (seen === undefined)
|
|
287
|
+
owner.set(key, index);
|
|
288
|
+
else
|
|
289
|
+
parent[find(index)] = find(seen);
|
|
296
290
|
}
|
|
291
|
+
});
|
|
292
|
+
const components = new Map();
|
|
293
|
+
humans.forEach((record, index) => {
|
|
294
|
+
const root = find(index);
|
|
295
|
+
components.set(root, [...(components.get(root) ?? []), record]);
|
|
296
|
+
});
|
|
297
|
+
const groups = [];
|
|
298
|
+
for (const members of components.values()) {
|
|
299
|
+
if (members.length < 2)
|
|
300
|
+
continue;
|
|
301
|
+
const [canonical, ...duplicates] = [...members].sort(canonicalOrder);
|
|
302
|
+
groups.push({ canonical: canonical, duplicates });
|
|
297
303
|
}
|
|
298
304
|
return groups.sort((a, b) => canonicalOrder(a.canonical, b.canonical));
|
|
299
305
|
}
|
|
@@ -311,7 +317,7 @@ async function dedupeFriends(input) {
|
|
|
311
317
|
targetId: group.canonical.id,
|
|
312
318
|
sourceIds: duplicateIds,
|
|
313
319
|
actor: input.actor,
|
|
314
|
-
reason: "duplicate
|
|
320
|
+
reason: "duplicate records of the same person",
|
|
315
321
|
dryRun,
|
|
316
322
|
...(input.now ? { now: input.now } : {}),
|
|
317
323
|
});
|
|
@@ -56,7 +56,7 @@ function deferredFileName(deliveryId) {
|
|
|
56
56
|
return `${(0, crypto_1.createHash)("sha256").update(deliveryId).digest("hex").slice(0, 32)}.json`;
|
|
57
57
|
}
|
|
58
58
|
function recordedText(record) {
|
|
59
|
-
return `[sent
|
|
59
|
+
return `[you sent this via ${record.originLabel}] ${record.content}`;
|
|
60
60
|
}
|
|
61
61
|
function recordError(message, meta) {
|
|
62
62
|
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "mind", event: "mind.outbound_record_error", message, meta });
|
|
@@ -68,7 +68,10 @@ function parseDeferred(raw) {
|
|
|
68
68
|
return null;
|
|
69
69
|
return value;
|
|
70
70
|
}
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* How a recorded message names the way it was sent: `[you sent this via <label>]`. Every record is the agent's own
|
|
73
|
+
* message, so a reply to it on any channel is a reply to the agent, which owns and can act on it.
|
|
74
|
+
*/
|
|
72
75
|
exports.OUTBOUND_ORIGIN_LABELS = {
|
|
73
76
|
shepherd: "Shepherd",
|
|
74
77
|
send_message: "send_message",
|
|
@@ -229,7 +229,7 @@ exports.shepherdSendToolDefinition = {
|
|
|
229
229
|
type: "function",
|
|
230
230
|
function: {
|
|
231
231
|
name: "shepherd_send",
|
|
232
|
-
description: `Type into one coding agent's terminal on this machine for the human you are talking with, because they asked you to. Use it only to submit that human's own instruction or keypress ("tell it to run the tests", "approve that", "stop it"): never on your own initiative, and never for anyone else. Give exactly one of text or key. text is one line (at most ${exports.SEND_TEXT_MAX_CHARS} characters, no control characters), typed as "[<your configured name> for <their name>] <text>" and submitted with Enter. key is pressed alone: one of ${exports.SEND_KEYS.join(", ")} (a menu or permission prompt, escape to interrupt, ctrl-c to cancel). enter, tab, a digit, y and n can approve or choose on a prompt, so they need screen from a shepherd_read made just before, and nothing is sent if the terminal changed since; tell the human what a prompt asks before approving it unless they have seen it. Pass screen with text too when the text answers what is on screen. To interrupt and then instruct, send escape, read again, then send the text. It also types into the terminal the human has focused on this machine. submission.state accepted means only that the terminal host accepted input; it may still be queued behind credential approval or a busy agent. It does not prove receiver receipt, resumed execution, restored capability or useful progress. Report those as unverified, never "unblocked" or "all unblocked" from this result or an unchanged screen. Each stronger claim needs fresh evidence from the exact target instance after this attempt, tied to this instruction; receipt alone is not progress or restored credentials. unknown may already have arrived: preserve the attempt and do not blindly resend.`,
|
|
232
|
+
description: `Type into one coding agent's terminal on this machine for the human you are talking with, because they asked you to. Use it only to submit that human's own instruction or keypress ("tell it to run the tests", "approve that", "stop it"): never on your own initiative, and never for anyone else. When they answer a Shepherd notice you sent them, on any channel, with something meant for that coding session, that is such a request: shepherd_read the session the notice names, check it still shows the agent and folder the notice named, send their answer, then tell them what you sent. A thank-you or a question for you is not; if it is unclear which session or what to send, ask. Give exactly one of text or key. text is one line (at most ${exports.SEND_TEXT_MAX_CHARS} characters, no control characters), typed as "[<your configured name> for <their name>] <text>" and submitted with Enter. key is pressed alone: one of ${exports.SEND_KEYS.join(", ")} (a menu or permission prompt, escape to interrupt, ctrl-c to cancel). enter, tab, a digit, y and n can approve or choose on a prompt, so they need screen from a shepherd_read made just before, and nothing is sent if the terminal changed since; tell the human what a prompt asks before approving it unless they have seen it. Pass screen with text too when the text answers what is on screen. To interrupt and then instruct, send escape, read again, then send the text. It also types into the terminal the human has focused on this machine. submission.state accepted means only that the terminal host accepted input; it may still be queued behind credential approval or a busy agent. It does not prove receiver receipt, resumed execution, restored capability or useful progress. Report those as unverified, never "unblocked" or "all unblocked" from this result or an unchanged screen. Each stronger claim needs fresh evidence from the exact target instance after this attempt, tied to this instruction; receipt alone is not progress or restored credentials. unknown may already have arrived: preserve the attempt and do not blindly resend.`,
|
|
233
233
|
parameters: {
|
|
234
234
|
type: "object",
|
|
235
235
|
properties: {
|
|
@@ -325,9 +325,10 @@ const defaultDeps = {
|
|
|
325
325
|
const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
|
|
326
326
|
const BLUEBUBBLES_RECOVERY_PASS_DELAY_MS = 1_000;
|
|
327
327
|
const BLUEBUBBLES_RECOVERY_PASS_INTERVAL_MS = 30_000;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
328
|
+
// A turn that is still thinking after this long is treated as hung. It is generous on purpose: a slow reply that
|
|
329
|
+
// arrives is worth far more than a dropped one, and a newer message from the person still supersedes an older turn.
|
|
330
|
+
const BLUEBUBBLES_TURN_TIMEOUT_MS = 10 * 60_000;
|
|
331
|
+
const BLUEBUBBLES_LIVE_TURN_STALLED_MS = 5 * 60_000;
|
|
331
332
|
const BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS = 2_000;
|
|
332
333
|
const BLUEBUBBLES_ACTIVITY_OPERATION_TIMEOUT_MS = 20_000;
|
|
333
334
|
const BLUEBUBBLES_CATCHUP_PAGE_SIZE = 50;
|
|
@@ -616,11 +617,6 @@ function getBlueBubblesContinuityIngressTexts(event) {
|
|
|
616
617
|
function sha256Hex(input) {
|
|
617
618
|
return (0, node_crypto_1.createHash)("sha256").update(input).digest("hex");
|
|
618
619
|
}
|
|
619
|
-
function timestampIsoForAutonomy(event) {
|
|
620
|
-
return Number.isFinite(event.timestamp)
|
|
621
|
-
? new Date(event.timestamp).toISOString()
|
|
622
|
-
: new Date().toISOString();
|
|
623
|
-
}
|
|
624
620
|
function blueBubblesAutonomyTarget(event) {
|
|
625
621
|
return {
|
|
626
622
|
messageGuid: event.messageGuid,
|
|
@@ -659,7 +655,6 @@ function recordBlueBubblesRecoveryFailureForBudget(agentName, event, source) {
|
|
|
659
655
|
try {
|
|
660
656
|
(0, autonomy_budget_1.recordAutonomyFailure)((0, identity_1.getAgentRoot)(agentName), {
|
|
661
657
|
...blueBubblesRecoveryStormInput(agentName, event, source),
|
|
662
|
-
occurredAt: timestampIsoForAutonomy(event),
|
|
663
658
|
});
|
|
664
659
|
}
|
|
665
660
|
catch {
|
|
@@ -1488,7 +1483,6 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1488
1483
|
senseOrHabit: "bluebubbles",
|
|
1489
1484
|
target: blueBubblesAutonomyTarget(event),
|
|
1490
1485
|
idempotencyKey: `bluebubbles-recovery:${source}:${event.messageGuid}`,
|
|
1491
|
-
now: timestampIsoForAutonomy(event),
|
|
1492
1486
|
storm: blueBubblesRecoveryStormInput(agentName, event, source),
|
|
1493
1487
|
});
|
|
1494
1488
|
if (!decision.allowed) {
|
|
@@ -1735,7 +1729,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1735
1729
|
const [groupHasFamilyMember, hasExistingGroupWithFamily] = trustContext;
|
|
1736
1730
|
lifecycleSignal?.throwIfAborted();
|
|
1737
1731
|
// ── Call shared pipeline ────────────────────────────────────────
|
|
1738
|
-
const timeoutMs = options.timeoutMs ??
|
|
1732
|
+
const timeoutMs = options.timeoutMs ?? BLUEBUBBLES_TURN_TIMEOUT_MS;
|
|
1739
1733
|
timeoutPromise = new Promise((resolve) => {
|
|
1740
1734
|
resolveTimeout = resolve;
|
|
1741
1735
|
});
|
|
@@ -2634,6 +2628,10 @@ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, so
|
|
|
2634
2628
|
publishAcceptedReceipt,
|
|
2635
2629
|
};
|
|
2636
2630
|
result = await handleBlueBubblesNormalizedEvent(canonicalEvent, resolvedDeps, source, capturedOptions);
|
|
2631
|
+
// A budget or storm-breaker block defers the message; it was never seen.
|
|
2632
|
+
// Leave it pending so a later recovery pass retries it once the block lifts.
|
|
2633
|
+
if (result.reason === "autonomy_budget_blocked")
|
|
2634
|
+
return result;
|
|
2637
2635
|
handledOutcome = result.notifiedAgent ? "message_completed" : "message_observed";
|
|
2638
2636
|
}
|
|
2639
2637
|
if (result.notifiedAgent) {
|
|
@@ -3022,7 +3020,7 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
|
|
|
3022
3020
|
}
|
|
3023
3021
|
semanticGenerationOwned = true;
|
|
3024
3022
|
const handled = await handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "upstream-catchup", {
|
|
3025
|
-
timeoutMs:
|
|
3023
|
+
timeoutMs: BLUEBUBBLES_TURN_TIMEOUT_MS,
|
|
3026
3024
|
autonomyBudgetTrigger: "recovery",
|
|
3027
3025
|
observationReservation,
|
|
3028
3026
|
lifecycleSignal: options.lifecycleSignal,
|
|
@@ -3138,7 +3136,7 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}, options = {}
|
|
|
3138
3136
|
normalized,
|
|
3139
3137
|
writeResult: "semantic_capture_duplicate",
|
|
3140
3138
|
}, resolvedDeps, "webhook", {
|
|
3141
|
-
timeoutMs:
|
|
3139
|
+
timeoutMs: BLUEBUBBLES_TURN_TIMEOUT_MS,
|
|
3142
3140
|
autonomyBudgetTrigger: "recovery",
|
|
3143
3141
|
semanticRecovery: true,
|
|
3144
3142
|
observationReservation,
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.NOTICE_RETRY_MS = exports.NOTICE_LIST_MAX = exports.NOTICE_COALESCE_MS = void 0;
|
|
36
|
+
exports.NOTICE_REPLY_LINE = exports.NOTICE_RETRY_MS = exports.NOTICE_LIST_MAX = exports.NOTICE_COALESCE_MS = void 0;
|
|
37
37
|
exports.noticeWanted = noticeWanted;
|
|
38
38
|
exports.plain = plain;
|
|
39
39
|
exports.noticeLine = noticeLine;
|
|
@@ -132,11 +132,13 @@ function noticeLine(record, location) {
|
|
|
132
132
|
case "working":
|
|
133
133
|
return `${subject} is still working in the background and has news. ${why}`;
|
|
134
134
|
case "unclear":
|
|
135
|
-
return `
|
|
135
|
+
return `I could not determine the current state of ${subject}. ${why}`;
|
|
136
136
|
default:
|
|
137
|
-
return `
|
|
137
|
+
return `I could not complete my observation of ${subject}; the agent's state was not determined. ${why}`;
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
+
/** Closes every notice: the person answers in the same conversation, and the agent relays it with `shepherd_send`. */
|
|
141
|
+
exports.NOTICE_REPLY_LINE = "Reply here and I'll pass your answer on.";
|
|
140
142
|
/** A bounded display of latest observations; all input records retain delivery custody. */
|
|
141
143
|
function noticeMessage(records, refs = new Map(), context = {}) {
|
|
142
144
|
const latest = new Map();
|
|
@@ -160,7 +162,7 @@ function noticeMessage(records, refs = new Map(), context = {}) {
|
|
|
160
162
|
const more = sessions.length - lines.length;
|
|
161
163
|
const body = sessions.length === 1 ? lines[0]
|
|
162
164
|
: [`Updates from ${sessions.length} coding sessions:`, ...lines.map(line => `- ${line}`), ...(more > 0 ? [`- and ${more} more.`] : [])].join("\n");
|
|
163
|
-
return context.recovered ? `Includes recovered historical observations, not live status:\n${body}` : body
|
|
165
|
+
return `${context.recovered ? `Includes recovered historical observations, not live status:\n${body}` : body}\n\n${exports.NOTICE_REPLY_LINE}`;
|
|
164
166
|
}
|
|
165
167
|
/**
|
|
166
168
|
* The channels this agent can reach people on from this machine: its enabled Teams sense, and its enabled BlueBubbles
|
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.953",
|
|
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.953",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|