@ouro.bot/cli 0.1.0-alpha.47 → 0.1.0-alpha.48
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 +8 -0
- package/dist/heart/turn-coordinator.js +5 -0
- package/dist/senses/bluebubbles.js +126 -123
- package/package.json +1 -1
- package/subagents/work-doer.md +1 -1
- package/subagents/work-planner.md +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
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.48",
|
|
6
|
+
"changes": [
|
|
7
|
+
"BlueBubbles same-chat turns now serialize through the shared heart-level turn coordinator, so duplicate delivery of one inbound message no longer races into two handled turns or duplicate replies.",
|
|
8
|
+
"BlueBubbles duplicate-check, session load, inbound turn execution, and inbound sidecar recording now happen inside one canonical chat-trunk critical section keyed by the resolved session path.",
|
|
9
|
+
"Workflow docs now let agents auto-create the required dedicated task worktree and branch by default when the human has not asked to control naming or layout."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
4
12
|
{
|
|
5
13
|
"version": "0.1.0-alpha.47",
|
|
6
14
|
"changes": [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createTurnCoordinator = createTurnCoordinator;
|
|
4
|
+
exports.withSharedTurnLock = withSharedTurnLock;
|
|
4
5
|
const runtime_1 = require("../nerves/runtime");
|
|
5
6
|
function createTurnCoordinator() {
|
|
6
7
|
const turnLocks = new Map();
|
|
@@ -60,3 +61,7 @@ function createTurnCoordinator() {
|
|
|
60
61
|
},
|
|
61
62
|
};
|
|
62
63
|
}
|
|
64
|
+
const _sharedTurnCoordinator = createTurnCoordinator();
|
|
65
|
+
function withSharedTurnLock(scope, key, fn) {
|
|
66
|
+
return _sharedTurnCoordinator.withTurnLock(`${scope}:${key}`, fn);
|
|
67
|
+
}
|
|
@@ -44,6 +44,7 @@ const path = __importStar(require("node:path"));
|
|
|
44
44
|
const core_1 = require("../heart/core");
|
|
45
45
|
const config_1 = require("../heart/config");
|
|
46
46
|
const identity_1 = require("../heart/identity");
|
|
47
|
+
const turn_coordinator_1 = require("../heart/turn-coordinator");
|
|
47
48
|
const context_1 = require("../mind/context");
|
|
48
49
|
const tokens_1 = require("../mind/friends/tokens");
|
|
49
50
|
const resolver_1 = require("../mind/friends/resolver");
|
|
@@ -547,143 +548,145 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
|
547
548
|
},
|
|
548
549
|
});
|
|
549
550
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
(0,
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
551
|
+
return (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", sessPath, async () => {
|
|
552
|
+
// Pre-load session inside the turn lock so same-chat deliveries cannot race on stale trunk state.
|
|
553
|
+
const existing = resolvedDeps.loadSession(sessPath);
|
|
554
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
555
|
+
? existing.messages
|
|
556
|
+
: [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", undefined, context) }];
|
|
557
|
+
if (event.kind === "message") {
|
|
558
|
+
const agentName = resolvedDeps.getAgentName();
|
|
559
|
+
if ((0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
|
|
560
|
+
(0, runtime_1.emitNervesEvent)({
|
|
561
|
+
component: "senses",
|
|
562
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
563
|
+
message: "skipped bluebubbles message already recorded as handled",
|
|
564
|
+
meta: {
|
|
565
|
+
messageGuid: event.messageGuid,
|
|
566
|
+
sessionKey: event.chat.sessionKey,
|
|
567
|
+
source,
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
571
|
+
}
|
|
572
|
+
if (source !== "webhook" && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)) {
|
|
573
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(agentName, event, "recovery-bootstrap");
|
|
574
|
+
(0, runtime_1.emitNervesEvent)({
|
|
575
|
+
component: "senses",
|
|
576
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
577
|
+
message: "skipped bluebubbles recovery because the session already contains the message text",
|
|
578
|
+
meta: {
|
|
579
|
+
messageGuid: event.messageGuid,
|
|
580
|
+
sessionKey: event.chat.sessionKey,
|
|
581
|
+
source,
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
585
|
+
}
|
|
569
586
|
}
|
|
570
|
-
|
|
571
|
-
|
|
587
|
+
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
588
|
+
const userMessage = {
|
|
589
|
+
role: "user",
|
|
590
|
+
content: buildInboundContent(event, existing?.messages ?? sessionMessages),
|
|
591
|
+
};
|
|
592
|
+
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget);
|
|
593
|
+
const controller = new AbortController();
|
|
594
|
+
// BB-specific tool context wrappers
|
|
595
|
+
const summarize = (0, core_1.createSummarize)();
|
|
596
|
+
const bbCapabilities = (0, channel_1.getChannelCapabilities)("bluebubbles");
|
|
597
|
+
const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
|
|
598
|
+
// ── Compute trust gate context for group/acquaintance rules ─────
|
|
599
|
+
const groupHasFamilyMember = await checkGroupHasFamilyMember(store, event);
|
|
600
|
+
const hasExistingGroupWithFamily = event.chat.isGroup
|
|
601
|
+
? false
|
|
602
|
+
: await checkHasExistingGroupWithFamily(store, context.friend);
|
|
603
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
604
|
+
try {
|
|
605
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
606
|
+
channel: "bluebubbles",
|
|
607
|
+
capabilities: bbCapabilities,
|
|
608
|
+
messages: [userMessage],
|
|
609
|
+
continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
|
|
610
|
+
callbacks,
|
|
611
|
+
friendResolver: { resolve: () => Promise.resolve(context) },
|
|
612
|
+
sessionLoader: { loadOrCreate: () => Promise.resolve({ messages: sessionMessages, sessionPath: sessPath, state: existing?.state }) },
|
|
613
|
+
pendingDir,
|
|
614
|
+
friendStore: store,
|
|
615
|
+
provider: "imessage-handle",
|
|
616
|
+
externalId: event.sender.externalId || event.sender.rawId,
|
|
617
|
+
isGroupChat: event.chat.isGroup,
|
|
618
|
+
groupHasFamilyMember,
|
|
619
|
+
hasExistingGroupWithFamily,
|
|
620
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
621
|
+
drainPending: pending_1.drainPending,
|
|
622
|
+
runAgent: (msgs, cb, channel, sig, opts) => resolvedDeps.runAgent(msgs, cb, channel, sig, {
|
|
623
|
+
...opts,
|
|
624
|
+
toolContext: {
|
|
625
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
626
|
+
signin: async () => undefined,
|
|
627
|
+
...opts?.toolContext,
|
|
628
|
+
summarize,
|
|
629
|
+
bluebubblesReplyTarget: {
|
|
630
|
+
setSelection: (selection) => replyTarget.setSelection(selection),
|
|
631
|
+
},
|
|
632
|
+
codingFeedback: {
|
|
633
|
+
send: async (message) => {
|
|
634
|
+
await client.sendText({
|
|
635
|
+
chat: event.chat,
|
|
636
|
+
text: message,
|
|
637
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
}),
|
|
643
|
+
postTurn: resolvedDeps.postTurn,
|
|
644
|
+
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
645
|
+
signal: controller.signal,
|
|
646
|
+
});
|
|
647
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
648
|
+
if (!result.gateResult.allowed) {
|
|
649
|
+
// Send auto-reply via BB API if the gate provides one
|
|
650
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
651
|
+
await client.sendText({
|
|
652
|
+
chat: event.chat,
|
|
653
|
+
text: result.gateResult.autoReply,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (event.kind === "message") {
|
|
657
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
handled: true,
|
|
661
|
+
notifiedAgent: false,
|
|
662
|
+
kind: event.kind,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
// Gate allowed — flush the agent's reply
|
|
666
|
+
await callbacks.flush();
|
|
667
|
+
if (event.kind === "message") {
|
|
668
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
|
|
669
|
+
}
|
|
572
670
|
(0, runtime_1.emitNervesEvent)({
|
|
573
671
|
component: "senses",
|
|
574
|
-
event: "senses.
|
|
575
|
-
message: "
|
|
672
|
+
event: "senses.bluebubbles_turn_end",
|
|
673
|
+
message: "bluebubbles event handled",
|
|
576
674
|
meta: {
|
|
577
675
|
messageGuid: event.messageGuid,
|
|
676
|
+
kind: event.kind,
|
|
578
677
|
sessionKey: event.chat.sessionKey,
|
|
579
|
-
source,
|
|
580
678
|
},
|
|
581
679
|
});
|
|
582
|
-
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
586
|
-
const userMessage = {
|
|
587
|
-
role: "user",
|
|
588
|
-
content: buildInboundContent(event, existing?.messages ?? sessionMessages),
|
|
589
|
-
};
|
|
590
|
-
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget);
|
|
591
|
-
const controller = new AbortController();
|
|
592
|
-
// BB-specific tool context wrappers
|
|
593
|
-
const summarize = (0, core_1.createSummarize)();
|
|
594
|
-
const bbCapabilities = (0, channel_1.getChannelCapabilities)("bluebubbles");
|
|
595
|
-
const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
|
|
596
|
-
// ── Compute trust gate context for group/acquaintance rules ─────
|
|
597
|
-
const groupHasFamilyMember = await checkGroupHasFamilyMember(store, event);
|
|
598
|
-
const hasExistingGroupWithFamily = event.chat.isGroup
|
|
599
|
-
? false
|
|
600
|
-
: await checkHasExistingGroupWithFamily(store, context.friend);
|
|
601
|
-
// ── Call shared pipeline ──────────────────────────────────────────
|
|
602
|
-
try {
|
|
603
|
-
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
604
|
-
channel: "bluebubbles",
|
|
605
|
-
capabilities: bbCapabilities,
|
|
606
|
-
messages: [userMessage],
|
|
607
|
-
continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
|
|
608
|
-
callbacks,
|
|
609
|
-
friendResolver: { resolve: () => Promise.resolve(context) },
|
|
610
|
-
sessionLoader: { loadOrCreate: () => Promise.resolve({ messages: sessionMessages, sessionPath: sessPath, state: existing?.state }) },
|
|
611
|
-
pendingDir,
|
|
612
|
-
friendStore: store,
|
|
613
|
-
provider: "imessage-handle",
|
|
614
|
-
externalId: event.sender.externalId || event.sender.rawId,
|
|
615
|
-
isGroupChat: event.chat.isGroup,
|
|
616
|
-
groupHasFamilyMember,
|
|
617
|
-
hasExistingGroupWithFamily,
|
|
618
|
-
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
619
|
-
drainPending: pending_1.drainPending,
|
|
620
|
-
runAgent: (msgs, cb, channel, sig, opts) => resolvedDeps.runAgent(msgs, cb, channel, sig, {
|
|
621
|
-
...opts,
|
|
622
|
-
toolContext: {
|
|
623
|
-
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
624
|
-
signin: async () => undefined,
|
|
625
|
-
...opts?.toolContext,
|
|
626
|
-
summarize,
|
|
627
|
-
bluebubblesReplyTarget: {
|
|
628
|
-
setSelection: (selection) => replyTarget.setSelection(selection),
|
|
629
|
-
},
|
|
630
|
-
codingFeedback: {
|
|
631
|
-
send: async (message) => {
|
|
632
|
-
await client.sendText({
|
|
633
|
-
chat: event.chat,
|
|
634
|
-
text: message,
|
|
635
|
-
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
636
|
-
});
|
|
637
|
-
},
|
|
638
|
-
},
|
|
639
|
-
},
|
|
640
|
-
}),
|
|
641
|
-
postTurn: resolvedDeps.postTurn,
|
|
642
|
-
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
643
|
-
signal: controller.signal,
|
|
644
|
-
});
|
|
645
|
-
// ── Handle gate result ────────────────────────────────────────
|
|
646
|
-
if (!result.gateResult.allowed) {
|
|
647
|
-
// Send auto-reply via BB API if the gate provides one
|
|
648
|
-
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
649
|
-
await client.sendText({
|
|
650
|
-
chat: event.chat,
|
|
651
|
-
text: result.gateResult.autoReply,
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
if (event.kind === "message") {
|
|
655
|
-
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
|
|
656
|
-
}
|
|
657
680
|
return {
|
|
658
681
|
handled: true,
|
|
659
|
-
notifiedAgent:
|
|
682
|
+
notifiedAgent: true,
|
|
660
683
|
kind: event.kind,
|
|
661
684
|
};
|
|
662
685
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (event.kind === "message") {
|
|
666
|
-
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
|
|
686
|
+
finally {
|
|
687
|
+
await callbacks.finish();
|
|
667
688
|
}
|
|
668
|
-
|
|
669
|
-
component: "senses",
|
|
670
|
-
event: "senses.bluebubbles_turn_end",
|
|
671
|
-
message: "bluebubbles event handled",
|
|
672
|
-
meta: {
|
|
673
|
-
messageGuid: event.messageGuid,
|
|
674
|
-
kind: event.kind,
|
|
675
|
-
sessionKey: event.chat.sessionKey,
|
|
676
|
-
},
|
|
677
|
-
});
|
|
678
|
-
return {
|
|
679
|
-
handled: true,
|
|
680
|
-
notifiedAgent: true,
|
|
681
|
-
kind: event.kind,
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
finally {
|
|
685
|
-
await callbacks.finish();
|
|
686
|
-
}
|
|
689
|
+
});
|
|
687
690
|
}
|
|
688
691
|
async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
689
692
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
package/package.json
CHANGED
package/subagents/work-doer.md
CHANGED
|
@@ -9,7 +9,7 @@ You are a task executor. Read a doing.md file and execute all units sequentially
|
|
|
9
9
|
## On Startup
|
|
10
10
|
|
|
11
11
|
1. **Find task-doc directory**: Read project instructions (for example `AGENTS.md`) to determine where planning/doing docs live for this repo
|
|
12
|
-
2. **Confirm worktree**: Run from the dedicated task worktree required by the project. If the current checkout is shared, ambiguous, or not on the task branch,
|
|
12
|
+
2. **Confirm worktree**: Run from the dedicated task worktree required by the project. If the current checkout is shared, ambiguous, or not on the task branch, switch/create the correct worktree first when project instructions allow it. Only STOP to ask the user when they explicitly want to control naming/layout or automatic creation fails.
|
|
13
13
|
3. **Find doing doc**: Look for `YYYY-MM-DD-HHMM-doing-*.md` in that project-defined task-doc directory
|
|
14
14
|
4. If multiple found, ask which one
|
|
15
15
|
5. If none found, ask user for location
|
|
@@ -11,7 +11,7 @@ You are a task planner for coding work. Help the user define scope, then convert
|
|
|
11
11
|
**Determine task doc directory:**
|
|
12
12
|
1. Read project instructions (for example `AGENTS.md`) to find the canonical task-doc location for the current repo
|
|
13
13
|
2. Derive `AGENT` from the current git branch when the project uses agent-scoped task docs
|
|
14
|
-
3. Confirm the task is running from a dedicated task worktree when the project requires parallel agent work; if the checkout is shared or ambiguous,
|
|
14
|
+
3. Confirm the task is running from a dedicated task worktree when the project requires parallel agent work; if the checkout is shared or ambiguous, create/switch to the dedicated worktree yourself when project instructions allow it, and only STOP to ask the caller when they explicitly want to control naming/layout or automatic creation fails
|
|
15
15
|
4. Set `TASK_DIR` to the project-defined planning/doing directory
|
|
16
16
|
5. If the project-defined parent location exists but `TASK_DIR` does not, create it
|
|
17
17
|
6. If the project does not define a task-doc location, STOP and ask the user or caller where planning/doing docs should live
|