@nopeek/agent-bridge 0.7.24 → 0.7.25

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/dist/bot.js CHANGED
@@ -8,7 +8,7 @@ import { FALLBACK_REPLY, resolveBrain } from "./brain.js";
8
8
  import { beginTurn, finishTurn, lastTurnFor } from "./last-turn.js";
9
9
  import { FileStore } from "./storage.js";
10
10
  import { TurnPublisher } from "./tool-progress.js";
11
- import { ChannelTurn, coalesceFollowups, isBotEcho, isPlaceholderText, isStopRequest, wrapInterruptedFollowup, } from "./mid-turn.js";
11
+ import { ChannelTurn, coalesceFollowups, isBotEcho, isPlaceholderText, isStopRequest, wrapInterruptedFollowup, wrapUserTurn, } from "./mid-turn.js";
12
12
  import { buildInboundPrompt, describeStructured, hasInboundWork, isControlType, parseAttachments, saveInboundFiles, } from "./inbound-files.js";
13
13
  import { deliverOutbound, extractMedia, attachMediaFiles, stripMediaTags } from "./outbound-media.js";
14
14
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -737,7 +737,7 @@ export class BotRunner {
737
737
  published.markStarted();
738
738
  const live = new ChannelTurn();
739
739
  this.turns.set(channelId, live);
740
- const prompt = interrupted ? wrapInterruptedFollowup(text) : text;
740
+ const prompt = interrupted ? wrapInterruptedFollowup(text) : wrapUserTurn(text);
741
741
  let reply = "";
742
742
  const stat = beginTurn(this.info.handle, channelId, resolved.kind);
743
743
  try {
package/dist/bridge.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { BridgeConfig, Pairing, BrainSpec, BrainBackend } from "./config.js";
2
- export declare const VERSION = "0.7.23";
2
+ export declare const VERSION = "0.7.25";
3
3
  export interface PairRequest {
4
4
  pairingSecret: string;
5
5
  appId: string;
package/dist/bridge.js CHANGED
@@ -19,7 +19,7 @@ import { reportCapabilities } from "./capabilities.js";
19
19
  import { lastHermesApiHealth, probeHermesApi } from "./hermes-http.js";
20
20
  import { lastTurnGlobal, turnSnapshot } from "./last-turn.js";
21
21
  import { isBrainBackend } from "./config.js";
22
- export const VERSION = "0.7.23";
22
+ export const VERSION = "0.7.25";
23
23
  function hermesApiStatus(cfg) {
24
24
  const api = lastHermesApiHealth();
25
25
  return {
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { asHooks } from "./brain.js";
3
3
  import { hermesSessionHeaders } from "./mid-turn.js";
4
+ import { rememberHermesSessionId } from "./hermes-session-store.js";
4
5
  import { RECAP_HINT } from "./tool-progress.js";
5
6
  /** Skip data-URL vision parts above this so a gallery cannot blow the POST. */
6
7
  const MAX_IMAGE_DATA_URL_BYTES = 8 * 1024 * 1024;
@@ -120,6 +121,7 @@ export function hermesHttpBrain(cfg) {
120
121
  console.error(`${tag} HTTP ${res.status}: ${body.slice(0, 300)}`);
121
122
  return "";
122
123
  }
124
+ rememberHermesSessionId(ctx.channelId, res.headers.get("X-Hermes-Session-Id"));
123
125
  if (!res.body) {
124
126
  clearTimeout(idle);
125
127
  console.error(`${tag} empty body`);
@@ -0,0 +1,5 @@
1
+ /** Test-only. Pass null to restore the real bridge home. */
2
+ export declare function setBridgeHomeForTests(dir: string | null): void;
3
+ export declare function resolvedHermesSessionId(channelId: string): string;
4
+ export declare function rememberHermesSessionId(channelId: string, returnedId: string | null | undefined): void;
5
+ export declare function forgetHermesSessionId(channelId: string): void;
@@ -0,0 +1,65 @@
1
+ // Hermes compression rotates X-Hermes-Session-Id (parent → 20260821_…).
2
+ // The bridge used to keep sending the original nopeek-<channel> id, so every
3
+ // turn reloaded the uncompressed parent (301 mixed messages) and Olie snapped
4
+ // back to an old NoPeek backlog. Persist the returned id per channel.
5
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { dirname, join } from "node:path";
8
+ function fallbackId(channelId) {
9
+ return `nopeek-${channelId}`;
10
+ }
11
+ let homeOverride = null;
12
+ /** Test-only. Pass null to restore the real bridge home. */
13
+ export function setBridgeHomeForTests(dir) {
14
+ homeOverride = dir;
15
+ }
16
+ function storePath() {
17
+ const home = homeOverride || process.env.NOPEEK_BRIDGE_HOME || join(homedir(), ".nopeek-bridge");
18
+ return join(home, "hermes-sessions.json");
19
+ }
20
+ function readMap() {
21
+ const path = storePath();
22
+ if (!existsSync(path))
23
+ return {};
24
+ try {
25
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
26
+ if (!parsed || typeof parsed !== "object")
27
+ return {};
28
+ const out = {};
29
+ for (const [k, v] of Object.entries(parsed)) {
30
+ if (typeof v === "string" && v.trim())
31
+ out[k] = v.trim();
32
+ }
33
+ return out;
34
+ }
35
+ catch {
36
+ return {};
37
+ }
38
+ }
39
+ function writeMap(map) {
40
+ const path = storePath();
41
+ mkdirSync(dirname(path), { recursive: true });
42
+ const tmp = `${path}.tmp`;
43
+ writeFileSync(tmp, JSON.stringify(map, null, 2));
44
+ renameSync(tmp, path);
45
+ }
46
+ export function resolvedHermesSessionId(channelId) {
47
+ return readMap()[channelId] || fallbackId(channelId);
48
+ }
49
+ export function rememberHermesSessionId(channelId, returnedId) {
50
+ const next = (returnedId || "").trim();
51
+ if (!next)
52
+ return;
53
+ const map = readMap();
54
+ if (map[channelId] === next)
55
+ return;
56
+ map[channelId] = next;
57
+ writeMap(map);
58
+ }
59
+ export function forgetHermesSessionId(channelId) {
60
+ const map = readMap();
61
+ if (!(channelId in map))
62
+ return;
63
+ delete map[channelId];
64
+ writeMap(map);
65
+ }
@@ -20,6 +20,9 @@ export declare function isBotEcho(m: {
20
20
  } | null;
21
21
  }, botUserId: string): boolean;
22
22
  export declare function coalesceFollowups(texts: string[]): FollowupDecision;
23
+ /** Pinned on every turn so compaction leftovers cannot become the job. */
24
+ export declare const JOB_PIN: string;
25
+ export declare function wrapUserTurn(text: string): string;
23
26
  export declare function wrapInterruptedFollowup(text: string): string;
24
27
  export declare function hermesSessionId(channelId: string): string;
25
28
  export declare function hermesSessionHeaders(apiKey: string, channelId: string): Record<string, string>;
package/dist/mid-turn.js CHANGED
@@ -7,6 +7,7 @@
7
7
  //
8
8
  // This module is the shared policy: detect stop, coalesce a burst, abort the
9
9
  // in-flight turn, and keep one stable Hermes session per channel.
10
+ import { resolvedHermesSessionId } from "./hermes-session-store.js";
10
11
  const STOP_RE = /^(?:\/)?(?:please\s+)?stop(?:\s+please)?[.!]?$/i;
11
12
  export function isStopRequest(text) {
12
13
  return STOP_RE.test((text || "").trim());
@@ -41,11 +42,28 @@ export function coalesceFollowups(texts) {
41
42
  const kept = cleaned.filter((t) => !isStopRequest(t));
42
43
  return { action: "continue", text: kept.join("\n\n") };
43
44
  }
45
+ /**
46
+ * Keep this marker. Hermes api_server `is_mid_task_followup` looks for it and
47
+ * otherwise prepends its own "ONE combined task" note — that is what made
48
+ * "do all 5" merge with a compacted NoPeek backlog.
49
+ */
50
+ const INTERRUPT_MARKER = "[System note: You were already working on a task";
51
+ /** Pinned on every turn so compaction leftovers cannot become the job. */
52
+ export const JOB_PIN = "[System note: The message below is the only live job. " +
53
+ "Do not resume older unfinished todos, numbered lists, or other products " +
54
+ "from compacted history unless this message clearly continues that same job. " +
55
+ '"Do all 5" means the list in YOUR LAST REPLY, not an older backlog. ' +
56
+ "Latest user message wins.]";
57
+ export function wrapUserTurn(text) {
58
+ return `${JOB_PIN}\n\n${text}`;
59
+ }
44
60
  export function wrapInterruptedFollowup(text) {
45
- return ("[System note: You were already working on a task. The user interrupted. " +
46
- "Reassess the previous work together with this new message and continue as " +
47
- "ONE combined task. Do not start a separate independent job. If the new " +
48
- "message replaces the old task, switch to it.]\n\n" +
61
+ return (`${INTERRUPT_MARKER}. The user interrupted. ` +
62
+ "The new message below REPLACES the in-flight task unless it is a small " +
63
+ 'add-on to the same job (e.g. "also add X"). Do not resume older unfinished ' +
64
+ "todos or other products from compacted history. " +
65
+ '"Do all 5" means the list in your last reply, not an older backlog. ' +
66
+ "Latest user message wins.]\n\n" +
49
67
  text);
50
68
  }
51
69
  export function hermesSessionId(channelId) {
@@ -55,10 +73,11 @@ export function hermesSessionHeaders(apiKey, channelId) {
55
73
  const headers = {};
56
74
  if (!apiKey)
57
75
  return headers;
58
- const sid = hermesSessionId(channelId);
76
+ // Key stays the channel. Id follows compression rotations so we do not
77
+ // reload the uncompressed parent every turn.
59
78
  headers.authorization = `Bearer ${apiKey}`;
60
- headers["X-Hermes-Session-Id"] = sid;
61
- headers["X-Hermes-Session-Key"] = sid;
79
+ headers["X-Hermes-Session-Id"] = resolvedHermesSessionId(channelId);
80
+ headers["X-Hermes-Session-Key"] = hermesSessionId(channelId);
62
81
  return headers;
63
82
  }
64
83
  export class ChannelTurn {
@@ -14,7 +14,7 @@ export declare function parseHermesActivityLine(raw: string): ToolProgressEvent
14
14
  /** One Telegram-style line. Running events only; completed is silent. */
15
15
  export declare function formatToolLine(ev: ToolProgressEvent): string | null;
16
16
  /** Hint so Hermes ends a tool-using turn with a recap, like Telegram. */
17
- export declare const RECAP_HINT = "When this turn used tools, your LAST message must be ONE phone briefing (not split). Use **bold** labels and blank lines like Telegram: **What was wrong**, **What I did**, **Current state**. Put copyable commands in ``` fences. Never end on a tool list, a diff, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
17
+ export declare const RECAP_HINT = "The latest user message is the only live job. Do not resume compacted leftovers or an older numbered list from another product. When this turn used tools, your LAST message must be ONE phone briefing (not split). Use **bold** labels and blank lines like Telegram: **What was wrong**, **What I did**, **Current state**. Put copyable commands in ``` fences. Never end on a tool list, a diff, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
18
18
  /** Posted when tools ran but the model never wrote a human recap. */
19
19
  export declare const MISSING_BRIEFING = "Done. I finished that work. Ask if you want the recap: what was wrong, what I did, and where things stand.";
20
20
  export interface StreamHandle {
@@ -95,7 +95,7 @@ function tidyLabel(raw) {
95
95
  return s.length > 80 ? `${s.slice(0, 77)}...` : s;
96
96
  }
97
97
  /** Hint so Hermes ends a tool-using turn with a recap, like Telegram. */
98
- export const RECAP_HINT = "When this turn used tools, your LAST message must be ONE phone briefing (not split). Use **bold** labels and blank lines like Telegram: **What was wrong**, **What I did**, **Current state**. Put copyable commands in ``` fences. Never end on a tool list, a diff, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
98
+ export const RECAP_HINT = "The latest user message is the only live job. Do not resume compacted leftovers or an older numbered list from another product. When this turn used tools, your LAST message must be ONE phone briefing (not split). Use **bold** labels and blank lines like Telegram: **What was wrong**, **What I did**, **Current state**. Put copyable commands in ``` fences. Never end on a tool list, a diff, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
99
99
  /** Posted when tools ran but the model never wrote a human recap. */
100
100
  export const MISSING_BRIEFING = "Done. I finished that work. Ask if you want the recap: what was wrong, what I did, and where things stand.";
101
101
  /** Text in `full` that has not already been posted as commentary. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nopeek/agent-bridge",
3
- "version": "0.7.24",
3
+ "version": "0.7.25",
4
4
  "description": "Run your own agents as E2EE NoPeek bots. Pairs with one-time codes (multiple accounts per computer), runs every bot each account owns, and pipes messages to any command or webhook.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,6 +46,6 @@
46
46
  "start": "node dist/cli.js",
47
47
  "dev": "tsx src/cli.ts",
48
48
  "typecheck": "tsc -p tsconfig.json --noEmit",
49
- "test": "tsx --test --test-concurrency=1 src/tool-progress.test.ts src/inbound-files.test.ts src/mid-turn.test.ts src/outbound-media.test.ts"
49
+ "test": "tsx --test --test-concurrency=1 src/tool-progress.test.ts src/inbound-files.test.ts src/mid-turn.test.ts src/outbound-media.test.ts src/hermes-session-store.test.ts"
50
50
  }
51
51
  }