@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 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
- // Pre-load session (adapter needs existing messages for lane history in content building)
551
- const existing = resolvedDeps.loadSession(sessPath);
552
- const sessionMessages = existing?.messages && existing.messages.length > 0
553
- ? existing.messages
554
- : [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", undefined, context) }];
555
- if (event.kind === "message") {
556
- const agentName = resolvedDeps.getAgentName();
557
- if ((0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
558
- (0, runtime_1.emitNervesEvent)({
559
- component: "senses",
560
- event: "senses.bluebubbles_recovery_skip",
561
- message: "skipped bluebubbles message already recorded as handled",
562
- meta: {
563
- messageGuid: event.messageGuid,
564
- sessionKey: event.chat.sessionKey,
565
- source,
566
- },
567
- });
568
- return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
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
- if (source !== "webhook" && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)) {
571
- (0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(agentName, event, "recovery-bootstrap");
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.bluebubbles_recovery_skip",
575
- message: "skipped bluebubbles recovery because the session already contains the message text",
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: false,
682
+ notifiedAgent: true,
660
683
  kind: event.kind,
661
684
  };
662
685
  }
663
- // Gate allowed — flush the agent's reply
664
- await callbacks.flush();
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
- (0, runtime_1.emitNervesEvent)({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.47",
3
+ "version": "0.1.0-alpha.48",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -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, STOP and switch/create the correct worktree first.
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, STOP and tell the caller to create/switch to a dedicated worktree first
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