@ouro.bot/cli 0.1.0-alpha.951 → 0.1.0-alpha.952
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
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
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.952",
|
|
6
|
+
"changes": [
|
|
7
|
+
"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.",
|
|
8
|
+
"`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."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
4
11
|
{
|
|
5
12
|
"version": "0.1.0-alpha.951",
|
|
6
13
|
"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.952</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
|
});
|
|
@@ -616,11 +616,6 @@ function getBlueBubblesContinuityIngressTexts(event) {
|
|
|
616
616
|
function sha256Hex(input) {
|
|
617
617
|
return (0, node_crypto_1.createHash)("sha256").update(input).digest("hex");
|
|
618
618
|
}
|
|
619
|
-
function timestampIsoForAutonomy(event) {
|
|
620
|
-
return Number.isFinite(event.timestamp)
|
|
621
|
-
? new Date(event.timestamp).toISOString()
|
|
622
|
-
: new Date().toISOString();
|
|
623
|
-
}
|
|
624
619
|
function blueBubblesAutonomyTarget(event) {
|
|
625
620
|
return {
|
|
626
621
|
messageGuid: event.messageGuid,
|
|
@@ -659,7 +654,6 @@ function recordBlueBubblesRecoveryFailureForBudget(agentName, event, source) {
|
|
|
659
654
|
try {
|
|
660
655
|
(0, autonomy_budget_1.recordAutonomyFailure)((0, identity_1.getAgentRoot)(agentName), {
|
|
661
656
|
...blueBubblesRecoveryStormInput(agentName, event, source),
|
|
662
|
-
occurredAt: timestampIsoForAutonomy(event),
|
|
663
657
|
});
|
|
664
658
|
}
|
|
665
659
|
catch {
|
|
@@ -1488,7 +1482,6 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1488
1482
|
senseOrHabit: "bluebubbles",
|
|
1489
1483
|
target: blueBubblesAutonomyTarget(event),
|
|
1490
1484
|
idempotencyKey: `bluebubbles-recovery:${source}:${event.messageGuid}`,
|
|
1491
|
-
now: timestampIsoForAutonomy(event),
|
|
1492
1485
|
storm: blueBubblesRecoveryStormInput(agentName, event, source),
|
|
1493
1486
|
});
|
|
1494
1487
|
if (!decision.allowed) {
|
|
@@ -2634,6 +2627,10 @@ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, so
|
|
|
2634
2627
|
publishAcceptedReceipt,
|
|
2635
2628
|
};
|
|
2636
2629
|
result = await handleBlueBubblesNormalizedEvent(canonicalEvent, resolvedDeps, source, capturedOptions);
|
|
2630
|
+
// A budget or storm-breaker block defers the message; it was never seen.
|
|
2631
|
+
// Leave it pending so a later recovery pass retries it once the block lifts.
|
|
2632
|
+
if (result.reason === "autonomy_budget_blocked")
|
|
2633
|
+
return result;
|
|
2637
2634
|
handledOutcome = result.notifiedAgent ? "message_completed" : "message_observed";
|
|
2638
2635
|
}
|
|
2639
2636
|
if (result.notifiedAgent) {
|
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.952",
|
|
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.952",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|