@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 +2 -2
- package/dist/bridge.d.ts +1 -1
- package/dist/bridge.js +1 -1
- package/dist/hermes-http.js +2 -0
- package/dist/hermes-session-store.d.ts +5 -0
- package/dist/hermes-session-store.js +65 -0
- package/dist/mid-turn.d.ts +3 -0
- package/dist/mid-turn.js +26 -7
- package/dist/tool-progress.d.ts +1 -1
- package/dist/tool-progress.js +1 -1
- package/package.json +2 -2
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
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.
|
|
22
|
+
export const VERSION = "0.7.25";
|
|
23
23
|
function hermesApiStatus(cfg) {
|
|
24
24
|
const api = lastHermesApiHealth();
|
|
25
25
|
return {
|
package/dist/hermes-http.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/mid-turn.d.ts
CHANGED
|
@@ -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 (
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
"
|
|
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
|
-
|
|
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"] =
|
|
61
|
-
headers["X-Hermes-Session-Key"] =
|
|
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 {
|
package/dist/tool-progress.d.ts
CHANGED
|
@@ -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 {
|
package/dist/tool-progress.js
CHANGED
|
@@ -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.
|
|
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
|
}
|