@agentvault/grok-bridge 0.4.24 → 0.4.26
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/README.md +1 -0
- package/dist/bridge.d.ts +42 -3
- package/dist/identity-prompt.d.ts +11 -0
- package/dist/index.js +645 -109
- package/dist/room-context.d.ts +101 -0
- package/dist/service/self-uninstall.d.ts +30 -2
- package/dist/service/self-uninstall.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room context for messages this agent reads but does not answer (2026-10-26).
|
|
3
|
+
*
|
|
4
|
+
* Under the #1107 rule an agent owes a reply only when it is @-mentioned (or
|
|
5
|
+
* @all / the owner rules in `owesRoomReply`). The bridge used to push EVERY room
|
|
6
|
+
* message into the model session anyway, and let the session suppress the
|
|
7
|
+
* reply. That bought the agent a full model turn per message it was never going
|
|
8
|
+
* to answer — measured on loopita (Opus, 1M context) at ~$0.27–0.31 per
|
|
9
|
+
* unaddressed message, ~$4–5 a day, for output that was thrown away.
|
|
10
|
+
*
|
|
11
|
+
* Now an unaddressed message costs nothing. It is held here, per room, and
|
|
12
|
+
* handed to the model as one block of context in front of the next message in
|
|
13
|
+
* that room that IS addressed to this agent — one turn, not N+1. Hermes reached
|
|
14
|
+
* the same place by a different road (`_record_room_message` +
|
|
15
|
+
* `_room_history_block` in plugin-hermes secure_channel.py): "read, not
|
|
16
|
+
* answered", kept, replayed on the turn it takes.
|
|
17
|
+
*
|
|
18
|
+
* The buffer is cleared once delivered. Unlike Hermes, these bridges drive a
|
|
19
|
+
* PERSISTENT model session, so once the block has been delivered the session
|
|
20
|
+
* remembers it — replaying it again would show the model the same messages
|
|
21
|
+
* twice.
|
|
22
|
+
*
|
|
23
|
+
* Bounded on purpose: per room by message count AND characters (oldest dropped
|
|
24
|
+
* first), and by number of rooms (least recently used room dropped). A room the
|
|
25
|
+
* agent is never addressed in must not grow without limit.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ The block lands INSIDE the turn that answers someone else — often the
|
|
28
|
+
* owner, and on some bridges a turn with tools. Every held message was written
|
|
29
|
+
* by another room member, possibly another tenant's agent, so nothing in it may
|
|
30
|
+
* be able to pass for bridge framing (PR #1458 review). Three layers:
|
|
31
|
+
* 1. sender names lose line breaks and brackets (`sanitizeLabel`);
|
|
32
|
+
* 2. every held text line is prefixed with "> ", so no held line can start
|
|
33
|
+
* with "[" and pose as a `[room … | owner]:` turn line;
|
|
34
|
+
* 3. the block is wrapped in delimiters carrying a fresh random id per turn,
|
|
35
|
+
* which a held message cannot know in advance and so cannot close early.
|
|
36
|
+
*
|
|
37
|
+
* COPIED from packages/claude-room-bridge/src/room-context.ts (2026-10-26).
|
|
38
|
+
* This file is OWNED by the Grok bridge — edit it freely. It is NOT
|
|
39
|
+
* shared, and a fix on the Claude side does NOT reach it.
|
|
40
|
+
*
|
|
41
|
+
* Unlike the Claude bridge there is no tool-capable worker lane here: every
|
|
42
|
+
* turn goes to the one persistent session, which before this change had seen
|
|
43
|
+
* each held message as its own turn. So the block is simply placed in front of
|
|
44
|
+
* the addressed message's text.
|
|
45
|
+
*/
|
|
46
|
+
export interface RoomContextEntry {
|
|
47
|
+
senderName: string;
|
|
48
|
+
text: string;
|
|
49
|
+
/** ISO time the message was sent (from the emit), or received if absent. */
|
|
50
|
+
at: string;
|
|
51
|
+
}
|
|
52
|
+
export interface RoomContextLimits {
|
|
53
|
+
/** Messages kept per room. */
|
|
54
|
+
maxMessages: number;
|
|
55
|
+
/** Characters kept per room (sender + text), oldest dropped first. */
|
|
56
|
+
maxChars: number;
|
|
57
|
+
/** Rooms tracked at once; the least recently used room is dropped. */
|
|
58
|
+
maxRooms: number;
|
|
59
|
+
/** One message's text is cut to this many characters. */
|
|
60
|
+
maxEntryChars: number;
|
|
61
|
+
}
|
|
62
|
+
export declare const DEFAULT_ROOM_CONTEXT_LIMITS: RoomContextLimits;
|
|
63
|
+
/**
|
|
64
|
+
* Cut `s` to at most `n` UTF-16 units without splitting a surrogate pair — a
|
|
65
|
+
* lone high surrogate left at the cut is invalid text (and garbles the turn).
|
|
66
|
+
*/
|
|
67
|
+
export declare function cutSafe(s: string, n: number): string;
|
|
68
|
+
/**
|
|
69
|
+
* A display name or room name made safe to sit inside a bracketed framing
|
|
70
|
+
* line: no control characters or line breaks (a name with a newline would start
|
|
71
|
+
* a line of its own), no square brackets (which would close the frame early).
|
|
72
|
+
* Used for held senders AND for the live turn prefix (`roomTurnPrefix`).
|
|
73
|
+
*/
|
|
74
|
+
export declare function sanitizeLabel(name: string, max?: number): string;
|
|
75
|
+
/** A fresh per-turn delimiter id: 12 hex chars from 6 random bytes. */
|
|
76
|
+
export declare function newContextId(): string;
|
|
77
|
+
export declare class RoomContextBuffer {
|
|
78
|
+
private readonly limits;
|
|
79
|
+
/** Insertion order IS recency order: a touched room is re-inserted at the end. */
|
|
80
|
+
private rooms;
|
|
81
|
+
constructor(limits?: Partial<RoomContextLimits>);
|
|
82
|
+
add(roomId: string, entry: RoomContextEntry): void;
|
|
83
|
+
/** The held messages for a room, oldest first. Does not clear. */
|
|
84
|
+
peek(roomId: string): {
|
|
85
|
+
entries: readonly RoomContextEntry[];
|
|
86
|
+
dropped: number;
|
|
87
|
+
};
|
|
88
|
+
clear(roomId: string): void;
|
|
89
|
+
size(roomId: string): number;
|
|
90
|
+
chars(roomId: string): number;
|
|
91
|
+
roomCount(): number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The block placed in front of an addressed message. Empty string when there
|
|
95
|
+
* is nothing held — an empty "earlier in this room" header would claim the room
|
|
96
|
+
* was silent when it only means this process saw nothing (e.g. a restart).
|
|
97
|
+
*
|
|
98
|
+
* `id` defaults to a fresh random one; pass it only to make a test deterministic.
|
|
99
|
+
*/
|
|
100
|
+
export declare function formatRoomContext(entries: readonly RoomContextEntry[], roomLabel: string, dropped?: number, id?: string): string;
|
|
101
|
+
//# sourceMappingURL=room-context.d.ts.map
|
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import { type ServiceBackend } from "./backend.js";
|
|
2
|
+
/**
|
|
3
|
+
* #913 item 2 / 2026-10-25 — what it takes to remove this agent's service.
|
|
4
|
+
*
|
|
5
|
+
* `attachLifecycle` routes every terminal signal here, and most of them mean
|
|
6
|
+
* the device is ALIVE:
|
|
7
|
+
*
|
|
8
|
+
* session_replaced another session took over — the device still exists
|
|
9
|
+
* device_jwt_expired the JWT aged out — the device still exists
|
|
10
|
+
* reconnect_loop gave up reconnecting — the device still exists
|
|
11
|
+
*
|
|
12
|
+
* This used to key on the reason string `device_revoked`. That was not proof:
|
|
13
|
+
* on 2026-10-25 a PAUSED (suspended) device's reissue 403 was mapped to
|
|
14
|
+
* "device_revoked" and this function removed loopita's launchd service — the
|
|
15
|
+
* owner switched the agent back on and nothing was installed to resume.
|
|
16
|
+
*
|
|
17
|
+
* So it now keys on the channel's CONFIRMED verdict only. SecureChannel sets
|
|
18
|
+
* `deviceGone: true` on `auth_failed` exclusively after the public status
|
|
19
|
+
* endpoint answered REVOKED or the device row is gone (404). Any reason string
|
|
20
|
+
* without that verdict — including `device_revoked` — leaves the service alone.
|
|
21
|
+
* An older channel that never sets the flag therefore never uninstalls: the
|
|
22
|
+
* safe direction.
|
|
23
|
+
*/
|
|
24
|
+
export interface TerminalVerdict {
|
|
25
|
+
reason: string;
|
|
26
|
+
/** Set by SecureChannel only when the status endpoint CONFIRMED revoked/deleted. */
|
|
27
|
+
deviceGone?: boolean;
|
|
28
|
+
}
|
|
2
29
|
export interface SelfUninstallDeps {
|
|
3
30
|
backend?: ServiceBackend;
|
|
4
31
|
log?: (msg: string) => void;
|
|
5
32
|
}
|
|
6
33
|
/**
|
|
7
|
-
* Remove this agent's supervised service after its device was
|
|
34
|
+
* Remove this agent's supervised service after its device was CONFIRMED
|
|
35
|
+
* revoked or deleted.
|
|
8
36
|
*
|
|
9
37
|
* Returns whether a service was actually removed. NEVER throws: this runs on
|
|
10
38
|
* the shutdown path, and a cleanup that cannot happen must not stop the bridge
|
|
@@ -15,5 +43,5 @@ export interface SelfUninstallDeps {
|
|
|
15
43
|
* and LEAVES the credentials on disk. A false positive therefore costs the
|
|
16
44
|
* customer a re-install, not their identity.
|
|
17
45
|
*/
|
|
18
|
-
export declare function selfUninstallOnTerminal(
|
|
46
|
+
export declare function selfUninstallOnTerminal(verdict: TerminalVerdict, env: NodeJS.ProcessEnv, deps?: SelfUninstallDeps): boolean;
|
|
19
47
|
//# sourceMappingURL=self-uninstall.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"self-uninstall.d.ts","sourceRoot":"","sources":["../../src/service/self-uninstall.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"self-uninstall.d.ts","sourceRoot":"","sources":["../../src/service/self-uninstall.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAyCT"}
|
package/package.json
CHANGED