@nac3/forge-cli 1.0.48 → 1.0.49

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.
@@ -74,6 +74,19 @@ interface Ctx {
74
74
  setBrainProvider?: (mode: 'plan' | 'console') => Promise<boolean>;
75
75
  bridge?: import('./bridge.js').BridgeHandle | null;
76
76
  }
77
+ /** Pure ownership decision for a mobile chat command across N desktop
78
+ * instances (Pablo 2026-06-23). Exactly one instance must run each command.
79
+ * Run when: no slug (legacy mobile -> old broadcast); the slug is MY active
80
+ * project; I am alone / no bridge; or it is an orphan project (no live peer
81
+ * owns it) and I am the lowest-id instance. Skip when a live peer owns it. */
82
+ export declare function ownsMobileCommand(opts: {
83
+ projectSlug?: string;
84
+ activeSlug: string | null;
85
+ peerSlugs: Set<string>;
86
+ liveIds: string[];
87
+ selfId: string;
88
+ hasBridge: boolean;
89
+ }): boolean;
77
90
  export declare function buildSystemPrompt(ctx: Ctx, mode?: ForgeMode, pilotState?: {
78
91
  pilot_completed: boolean;
79
92
  target_pending: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/chat/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/chat/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAKjG,OAAO,EAAoD,KAAK,aAAa,EAAoB,MAAM,aAAa,CAAC;AAErH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAEL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AAczB,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAsBjD,OAAO,EAAyD,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAc1H,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;;;;;wDAQoD;IACpD,YAAY,CAAC,EAAE,eAAe,GAAG,iBAAiB,CAAC;IACnD;;2DAEuD;IACvD,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;+CAC2C;IAC3C,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;kEAG8D;IAC9D,OAAO,CAAC,EAAE,OAAO,sBAAsB,EAAE,OAAO,CAAC;CAClD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ;qEACiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAqBD,wBAAsB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CA6ahF;AAED,UAAU,GAAG;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,WAAW,CAAC;IACnB,KAAK,EAAE,eAAe,CAAC;IAIvB,OAAO,CAAC,EAAE,OAAO,sBAAsB,EAAE,OAAO,CAAC;IAMjD,YAAY,CAAC,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAKnD,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAG3G,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAGlE,MAAM,CAAC,EAAE,OAAO,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;CACpD;AA0BD;;;;+EAI+E;AAC/E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAOV;AAq3JD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,GAAG,EACR,IAAI,GAAE,SAAuB,EAC7B,UAAU,GAAE;IAAE,eAAe,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAErF;AACD;;;qDAGqD;AACrD,KAAK,CAAC,EAAE;IAAE,IAAI,EAAE,KAAK,GAAG,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACrE;;;;;2CAK2C;AAC3C,sBAAsB,GAAE,MAAW;AACnC;;;;;;;4DAO4D;AAC5D,YAAY,GAAE,OAAe;AAC7B;;;;;;;uDAOuD;AACvD,mBAAmB,GAAE,MAAW;AAChC;;;;kEAIkE;AAClE,gBAAgB,GAAE,MAAW;AAC7B;;;;;;;;;;;+BAW+B;AAC/B,SAAS,GAAE,QAAQ,GAAG,YAAY,GAAG,MAAiB;AACtD;;;;;mEAKmE;AACnE,eAAe,CAAC,EAAE,eAAe,GAChC,MAAM,CAsgBR;AAED;;;;2EAI2E;AAC3E,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,CAoB5F"}
@@ -440,7 +440,15 @@ export async function startChatServer(opts) {
440
440
  * restart needed). When the relay is off it is a cheap no-op timer. */
441
441
  let commandConsumerHandle = null;
442
442
  try {
443
- commandConsumerHandle = startDesktopCommandConsumer(opts.port);
443
+ commandConsumerHandle = startDesktopCommandConsumer(opts.port, {
444
+ /* live so a reanchor moves which commands this instance owns */
445
+ getActiveSlug: () => session.projectSlug,
446
+ /* bridgeHandle is assigned just below; the closure reads it lazily at
447
+ * refresh time (after boot), so multi-instance ownership works without
448
+ * reordering. Null bridge -> single-instance assumption (run all). */
449
+ getBridgeState: async () => (bridgeHandle ? bridgeHandle.state() : null),
450
+ getSelfId: () => bridgeHandle?.selfId ?? '',
451
+ });
444
452
  }
445
453
  catch {
446
454
  commandConsumerHandle = null;
@@ -575,8 +583,68 @@ function getRelayPublisher() {
575
583
  * paired phone's chat commands, runs each through THIS server's own
576
584
  * /api/chat (loopback, so the full BYOK brain + project context apply),
577
585
  * and publishes the reply back as chat_reply. Gated YF_RELAY=1. */
578
- function startDesktopCommandConsumer(port) {
586
+ /** Pure ownership decision for a mobile chat command across N desktop
587
+ * instances (Pablo 2026-06-23). Exactly one instance must run each command.
588
+ * Run when: no slug (legacy mobile -> old broadcast); the slug is MY active
589
+ * project; I am alone / no bridge; or it is an orphan project (no live peer
590
+ * owns it) and I am the lowest-id instance. Skip when a live peer owns it. */
591
+ export function ownsMobileCommand(opts) {
592
+ if (!opts.projectSlug)
593
+ return true;
594
+ if (opts.projectSlug === opts.activeSlug)
595
+ return true;
596
+ if (!opts.hasBridge || opts.liveIds.length <= 1)
597
+ return true;
598
+ if (opts.peerSlugs.has(opts.projectSlug))
599
+ return false;
600
+ const lowest = [...opts.liveIds].sort()[0];
601
+ return opts.selfId !== '' && opts.selfId === lowest;
602
+ }
603
+ function startDesktopCommandConsumer(port, coord = {}) {
579
604
  const deviceId = 'desktop-' + process.pid;
605
+ /* Multi-instance project integrity (Pablo 2026-06-23). The relay is keyed
606
+ * only by handle, so EVERY yf chat on the machine reads the SAME command
607
+ * stream -> a phone command would run on ALL of them (duplicate execution,
608
+ * N replies). With the per-turn project binding, the phone tags each chat
609
+ * command with its project_slug; this lets each instance decide if the
610
+ * command is "mine". Rule: run it when (a) no slug (legacy mobile, keep old
611
+ * broadcast), (b) the slug is MY active project, or (c) no OTHER live
612
+ * instance owns that project (single instance, or an orphan project where
613
+ * the lowest-id instance takes it). So exactly ONE instance runs each
614
+ * command. Decision is sync off a cached bridge snapshot refreshed below. */
615
+ let peerSnapshot = { slugs: new Set(), liveIds: [], hasBridge: false };
616
+ const PEER_FRESH_MS = 15_000;
617
+ const refreshPeers = async () => {
618
+ if (!coord.getBridgeState)
619
+ return;
620
+ try {
621
+ const st = await coord.getBridgeState();
622
+ if (!st || !Array.isArray(st.instances)) {
623
+ peerSnapshot = { slugs: new Set(), liveIds: [], hasBridge: false };
624
+ return;
625
+ }
626
+ const cut = Date.now() - PEER_FRESH_MS;
627
+ const live = st.instances.filter((i) => i && typeof i.last_seen === 'number' && i.last_seen >= cut);
628
+ const self = coord.getSelfId ? coord.getSelfId() : '';
629
+ peerSnapshot = {
630
+ slugs: new Set(live.filter((i) => i.id !== self).map((i) => String(i.project_slug || '')).filter(Boolean)),
631
+ liveIds: live.map((i) => i.id),
632
+ hasBridge: true,
633
+ };
634
+ }
635
+ catch { /* keep the last snapshot on a transient failure */ }
636
+ };
637
+ const peerTimer = setInterval(() => { void refreshPeers(); }, 2500);
638
+ if (typeof peerTimer.unref === 'function')
639
+ peerTimer.unref();
640
+ const ownsChatCommand = (projectSlug) => ownsMobileCommand({
641
+ ...(projectSlug ? { projectSlug } : {}),
642
+ activeSlug: coord.getActiveSlug ? (coord.getActiveSlug() ?? null) : null,
643
+ peerSlugs: peerSnapshot.slugs,
644
+ liveIds: peerSnapshot.liveIds,
645
+ selfId: coord.getSelfId ? coord.getSelfId() : '',
646
+ hasBridge: peerSnapshot.hasBridge,
647
+ });
580
648
  const pollCommands = async (since) => {
581
649
  const r = await relayPoll({ deviceId }, since, ['command']);
582
650
  return {
@@ -709,7 +777,9 @@ function startDesktopCommandConsumer(port) {
709
777
  * past the skipped events. */
710
778
  const chat = startCommandConsumer({
711
779
  enabled: relayEnabled, intervalMs: 3000,
712
- accept: (c) => !c.control,
780
+ /* only MY project's chat commands (multi-instance integrity); the other
781
+ * instances' consumers advance their cursor past the skipped ones. */
782
+ accept: (c) => !c.control && ownsChatCommand(c.projectSlug),
713
783
  pollCommands, runTurn, reply,
714
784
  ackImmediate: true, /* confirm receipt before the turn runs */
715
785
  onError: () => { },
@@ -721,8 +791,8 @@ function startDesktopCommandConsumer(port) {
721
791
  onError: () => { },
722
792
  });
723
793
  return {
724
- stop() { chat.stop(); control.stop(); },
725
- tickOnce: async () => { await chat.tickOnce(); await control.tickOnce(); },
794
+ stop() { clearInterval(peerTimer); chat.stop(); control.stop(); },
795
+ tickOnce: async () => { await refreshPeers(); await chat.tickOnce(); await control.tickOnce(); },
726
796
  };
727
797
  }
728
798
  /* Project context for board snapshots, set per chat turn (handleChat). */