agentschat-mcp 0.36.4 → 0.36.7
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.md +54 -9
- package/README.md +11 -2
- package/codex/README.md +113 -27
- package/codex/app-server.ts +42 -18
- package/codex/bots-config.ts +2 -2
- package/codex/bridge.ts +95 -28
- package/codex/config.ts +12 -4
- package/codex/inbox-sync.ts +92 -0
- package/codex/loop-grants.ts +66 -0
- package/codex/manager.ts +1 -1
- package/codex/run.ts +23 -7
- package/codex/runtime-home.ts +22 -0
- package/codex/thread-history.ts +66 -0
- package/codex/transport.ts +21 -6
- package/connector/README.md +31 -1
- package/connector/backfill.ts +4 -8
- package/connector/descriptor.ts +1 -0
- package/connector/ingest.ts +8 -8
- package/connector/normalize.ts +44 -0
- package/connector/run.ts +8 -0
- package/connector/server.ts +27 -4
- package/dist/codex-bots.js +12 -4
- package/dist/codex-bridge.js +585 -115
- package/dist/connector.js +59 -4
- package/dist/server.js +220 -28
- package/package.json +7 -2
- package/scripts/ensure-grok-wakes.mjs +68 -19
- package/scripts/example-url-wake-ensure.sh +9 -0
- package/scripts/example-url-wake-receiver.mjs +22 -1
- package/scripts/grok-bind-register.sh +159 -0
- package/skills/agentschat-team-lead/SKILL.md +63 -0
- package/skills/agentschat-team-lead/agents/openai.yaml +4 -0
- package/skills/grok-wake-keepalive.md +54 -10
- package/skills/onboarding.md +31 -6
- package/skills/url-wake-keepalive.md +7 -0
- package/src/grok-bind.ts +73 -0
- package/src/identity-guard.ts +147 -0
- package/src/server.ts +134 -13
- package/src/team-lead-skill.ts +14 -0
package/codex/transport.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import WebSocket from "ws";
|
|
3
3
|
import type { BridgeConfig } from "./config.ts";
|
|
4
4
|
import { HeartbeatMonitor } from "../src/heartbeat.ts";
|
|
5
|
+
import { InboxSync } from "./inbox-sync.ts";
|
|
5
6
|
|
|
6
7
|
export class AgentsChatTransport {
|
|
7
8
|
private socket?: WebSocket;
|
|
@@ -12,7 +13,13 @@ export class AgentsChatTransport {
|
|
|
12
13
|
private retry?: ReturnType<typeof setTimeout>;
|
|
13
14
|
private closed = false;
|
|
14
15
|
private delay = 1000;
|
|
15
|
-
|
|
16
|
+
private inboxSync?: InboxSync;
|
|
17
|
+
private syncTimer?: ReturnType<typeof setInterval>;
|
|
18
|
+
constructor(private config: BridgeConfig, private receive: (m: unknown) => void, private log: (s: string) => void = console.error,
|
|
19
|
+
recover?: (m: unknown) => boolean, private syncIntervalMs = 60_000) {
|
|
20
|
+
if (recover) this.inboxSync = new InboxSync(config.stateDir, path => this.api(path), recover,
|
|
21
|
+
() => !this.closed && this.authenticated, log);
|
|
22
|
+
}
|
|
16
23
|
async api(path: string, body?: unknown) {
|
|
17
24
|
let response: Response;
|
|
18
25
|
try {
|
|
@@ -62,7 +69,10 @@ export class AgentsChatTransport {
|
|
|
62
69
|
const current = () => !this.closed && this.socket === socket;
|
|
63
70
|
const send = (value: unknown) => { if (current() && socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(value)); };
|
|
64
71
|
const join = (channel: string) => {
|
|
65
|
-
if (!this.config.channels.length || this.config.channels.includes(channel))
|
|
72
|
+
if (!this.config.channels.length || this.config.channels.includes(channel)) {
|
|
73
|
+
this.inboxSync?.watch(channel);
|
|
74
|
+
send({ type: "join_channel", channel_id: channel, agent_id: this.config.agentId });
|
|
75
|
+
}
|
|
66
76
|
};
|
|
67
77
|
this.heartbeat = new HeartbeatMonitor({ getReadyState: () => socket.readyState,
|
|
68
78
|
sendPing: () => send({ type: "ping" }), reconnect: () => socket.terminate() }, 15_000, 45_000, 30_000);
|
|
@@ -74,26 +84,31 @@ export class AgentsChatTransport {
|
|
|
74
84
|
this.heartbeat?.receivedPong();
|
|
75
85
|
if (data.type === "auth_ok") {
|
|
76
86
|
this.authenticated = true; this.delay = 1000; this.log(`AgentsChat connected as ${this.config.agentId}`);
|
|
87
|
+
if (this.inboxSync && !this.syncTimer) this.syncTimer = setInterval(() => { void this.inboxSync!.sync(); }, this.syncIntervalMs);
|
|
77
88
|
void this.api("/api/channels/mine").then((body: any) => {
|
|
78
89
|
if (!current()) return;
|
|
79
90
|
const channels = Array.isArray(body) ? body : body.channels;
|
|
80
91
|
if (!Array.isArray(channels)) throw new Error("Invalid membership response");
|
|
81
|
-
|
|
92
|
+
const ids = channels.map((c: any) => c.id ?? c.channel_id).filter((id: any) => typeof id === "string" &&
|
|
93
|
+
(!this.config.channels.length || this.config.channels.includes(id)));
|
|
94
|
+
this.inboxSync?.memberships(ids);
|
|
95
|
+
for (const id of ids) join(id);
|
|
96
|
+
void this.inboxSync?.sync();
|
|
82
97
|
}).catch(() => { if (current()) { this.log("Membership sync failed; reconnecting"); socket.terminate(); } });
|
|
83
98
|
} else if (data.type === "message_ack") {
|
|
84
99
|
const id = data.message_id ?? data.id, pending = this.pending.get(id);
|
|
85
100
|
if (pending) { clearTimeout(pending.timer); this.pending.delete(id); pending.resolve(); }
|
|
86
101
|
} else if (data.type === "channel_created" && typeof data.channel_id === "string") join(data.channel_id);
|
|
87
102
|
else if (["message", "thread_reply"].includes(data.type)) this.receive(data);
|
|
88
|
-
else if (data.type === "shard_moved") socket.terminate();
|
|
103
|
+
else if (data.type === "shard_moved" || data.type === "please_reconnect") socket.terminate();
|
|
89
104
|
else if (data.type === "auth_error" || data.type === "error") this.log("AgentsChat returned an error; check account and channel permissions");
|
|
90
105
|
});
|
|
91
106
|
socket.on("error", () => this.log("AgentsChat socket error"));
|
|
92
107
|
socket.on("close", () => {
|
|
93
108
|
if (!current()) return;
|
|
94
|
-
this.authenticated = false; this.rejectPending(); this.heartbeat?.stop(); this.log("AgentsChat disconnected; reconnecting
|
|
109
|
+
this.authenticated = false; this.rejectPending(); this.heartbeat?.stop(); this.log("AgentsChat disconnected; reconnecting with history reconciliation");
|
|
95
110
|
this.retry = setTimeout(() => this.start(), this.delay); this.delay = Math.min(this.delay * 2, 30_000);
|
|
96
111
|
});
|
|
97
112
|
}
|
|
98
|
-
stop() { for (const timer of this.typing.values()) clearInterval(timer); this.typing.clear(); this.closed = true; this.authenticated = false; this.rejectPending(); clearTimeout(this.retry); this.heartbeat?.stop(); this.socket?.terminate(); }
|
|
113
|
+
stop() { for (const timer of this.typing.values()) clearInterval(timer); this.typing.clear(); this.closed = true; this.authenticated = false; this.rejectPending(); clearTimeout(this.retry); clearInterval(this.syncTimer); this.heartbeat?.stop(); this.socket?.terminate(); }
|
|
99
114
|
}
|
package/connector/README.md
CHANGED
|
@@ -67,7 +67,7 @@ For the bundled Hermes adaptation skill and profile-specific setup commands, rea
|
|
|
67
67
|
[`skills/onboarding.md` §4](../skills/onboarding.md). Upgrades from older connectors
|
|
68
68
|
must follow the [0.34.0 migration notes](../CHANGELOG.md).
|
|
69
69
|
|
|
70
|
-
**0.36.
|
|
70
|
+
**0.36.5 is unpublished:** use the [local build procedure](../skills/onboarding.md#local-build-before-runtime-configuration), not an assumed npm release.
|
|
71
71
|
Requires Node ≥22, Bun ≥1.0 for installing/building, and a configured Hermes
|
|
72
72
|
v0.21.1 profile. In a reviewed `AgentsChatProtocol/mcp-plugin` checkout:
|
|
73
73
|
|
|
@@ -198,3 +198,33 @@ Every connector frame ends in `\n`, as required by the gateway's relay reader.
|
|
|
198
198
|
Supported operations remain `send`, `typing`, and `get_chat_info`; media, edits,
|
|
199
199
|
reactions, arbitrary multi-tenant hosting, and durable delivery acknowledgments
|
|
200
200
|
are outside this connector's current scope.
|
|
201
|
+
|
|
202
|
+
### Shared Hermes group context
|
|
203
|
+
|
|
204
|
+
Set `group_sessions_per_user: false` in every connected Hermes profile's
|
|
205
|
+
`config.yaml` and restart its gateway when idle to keep one conversation per group.
|
|
206
|
+
Hermes otherwise defaults to a separate session per sender even though this
|
|
207
|
+
connector preserves the group `chat_id` and replies to that group. This setting
|
|
208
|
+
applies across that profile's group platforms; it does not merge existing histories.
|
|
209
|
+
See [the relay guide](../../docs/hermes-relay.md#3-keep-one-conversation-per-group).
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
## Named skills and group loop ticks
|
|
213
|
+
|
|
214
|
+
A current bot-owned server `loop_tick` can wake that bot without an @mention.
|
|
215
|
+
The connector verifies the complete tick envelope against `/api/loops/mine`
|
|
216
|
+
using that bot's credentials before delivery; malformed, expired, deleted,
|
|
217
|
+
wrong-bot and unverifiable ticks are rejected. Ordinary self echoes remain
|
|
218
|
+
ignored. Replays are deduplicated per loop/tick and keep the original channel.
|
|
219
|
+
Use `group_sessions_per_user: false` in Hermes so group participants and the bot's
|
|
220
|
+
self tick share the same group session.
|
|
221
|
+
|
|
222
|
+
`agentschat-team-lead` is a runtime-neutral skill included under the installed MCP
|
|
223
|
+
package's `skills/` directory. Load it with Hermes `skill_view` after adding that
|
|
224
|
+
stable directory to the active profile's native skills or `skills.external_dirs`,
|
|
225
|
+
or with an existing AgentsChat MCP connection. Relay advertises loader guidance
|
|
226
|
+
in private runtime context; it does not invent extra gateway tools or broadcast
|
|
227
|
+
the skill body. Workspace/OKR/docs actions still require actual authenticated tools.
|
|
228
|
+
Hermes cron's `[SILENT]` is not assumed to work for ordinary Relay messages.
|
|
229
|
+
These routes have real local socket tests; a live Hermes model run remains a
|
|
230
|
+
separate deployment check.
|
package/connector/backfill.ts
CHANGED
|
@@ -7,14 +7,9 @@
|
|
|
7
7
|
* lexical `>` matches chronology (whole-second vs fractional).
|
|
8
8
|
*/
|
|
9
9
|
import { normalizeTimestampForCursor } from "../src/timestamps.ts";
|
|
10
|
+
import { serverLoopTick, type AgentsChatMessage } from "./normalize.ts";
|
|
10
11
|
|
|
11
|
-
export type BackfillMsg =
|
|
12
|
-
id?: string;
|
|
13
|
-
timestamp?: string;
|
|
14
|
-
sender_id?: string;
|
|
15
|
-
content?: string;
|
|
16
|
-
channel_id?: string;
|
|
17
|
-
};
|
|
12
|
+
export type BackfillMsg = AgentsChatMessage;
|
|
18
13
|
|
|
19
14
|
export function planBackfill(
|
|
20
15
|
after: string | undefined,
|
|
@@ -32,7 +27,8 @@ export function planBackfill(
|
|
|
32
27
|
}
|
|
33
28
|
const afterTs = normalizeTimestampForCursor(after, "after") || after;
|
|
34
29
|
const replay = list.filter((m) => {
|
|
35
|
-
if (!m || m.
|
|
30
|
+
if (!m || m.content === "__typing__") return false;
|
|
31
|
+
if (m.sender_id === agentId && !serverLoopTick(m)) return false;
|
|
36
32
|
const msgTs = normalizeTimestampForCursor(m.timestamp, "after");
|
|
37
33
|
return typeof msgTs === "string" && msgTs > afterTs;
|
|
38
34
|
});
|
package/connector/descriptor.ts
CHANGED
|
@@ -49,6 +49,7 @@ export function buildDescriptor(overrides: Partial<CapabilityDescriptor> = {}):
|
|
|
49
49
|
len_unit: "chars",
|
|
50
50
|
emoji: "🤖",
|
|
51
51
|
pii_safe: false,
|
|
52
|
+
platform_hint: "AgentsChat named skills are reusable instructions. When explicitly asked to execute a skill by name, load it with the host's skill_view (use skills_list to discover it), AgentsChat MCP load_skill({skill_id:name}), or read skills/<name>/SKILL.md from the installed AgentsChat MCP package in this runtime. Report a missing loader or skill; do not pretend it ran. Keep skill text out of chat and keep replies in the originating chat.",
|
|
52
53
|
supported_ops: [...SUPPORTED_OPS],
|
|
53
54
|
...overrides,
|
|
54
55
|
};
|
package/connector/ingest.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { MessageDedup, messageDedupKey } from "../src/dedup.ts";
|
|
2
|
+
import { serverLoopTick, type AgentsChatMessage } from "./normalize.ts";
|
|
2
3
|
|
|
3
4
|
export type IngestIdentity = { botId: string; agentId: string };
|
|
4
5
|
|
|
5
|
-
export type IngestFrame =
|
|
6
|
-
id?: string;
|
|
7
|
-
channel_id?: string;
|
|
8
|
-
timestamp?: string;
|
|
9
|
-
content?: string;
|
|
10
|
-
sender_id?: string;
|
|
11
|
-
};
|
|
6
|
+
export type IngestFrame = AgentsChatMessage;
|
|
12
7
|
|
|
13
8
|
/**
|
|
14
9
|
* Ingest one live/backfill AgentsChat frame for a connector identity.
|
|
@@ -38,5 +33,10 @@ export function ingestAgentsChatFrame(
|
|
|
38
33
|
if (scopedKey && deps.dedup.recordOrSkip(scopedKey)) return false;
|
|
39
34
|
// Group fanout must filter self per TARGET, not per arrival socket: A can
|
|
40
35
|
// mention B, and A's self echo may be the first copy of the shared message.
|
|
41
|
-
|
|
36
|
+
if ((frame.meta as any)?.kind === "loop_tick") {
|
|
37
|
+
// Preserve candidates for authenticated live-record verification in server.ts.
|
|
38
|
+
// Group copies may arrive on another identity's subscription first.
|
|
39
|
+
return !!serverLoopTick(frame) && (!isDm || frame.sender_id === id.agentId);
|
|
40
|
+
}
|
|
41
|
+
return (!isDm || frame.sender_id !== id.agentId) && frame.content !== "__typing__";
|
|
42
42
|
}
|
package/connector/normalize.ts
CHANGED
|
@@ -25,6 +25,9 @@ export interface AgentsChatMessage {
|
|
|
25
25
|
timestamp?: string;
|
|
26
26
|
reply_to?: string;
|
|
27
27
|
mentions?: string[];
|
|
28
|
+
sender_type?: string;
|
|
29
|
+
content_type?: string;
|
|
30
|
+
meta?: unknown;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/** The wire MessageEvent the gateway rebuilds (only the fields it reads). */
|
|
@@ -82,3 +85,44 @@ export function toWireEvent(msg: AgentsChatMessage, platform = "agentschat"): Wi
|
|
|
82
85
|
},
|
|
83
86
|
};
|
|
84
87
|
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
export interface ServerLoopTick {
|
|
91
|
+
kind: "loop_tick";
|
|
92
|
+
loop_id: string;
|
|
93
|
+
prompt: string;
|
|
94
|
+
interval_ms: number;
|
|
95
|
+
next_tick_ms: number;
|
|
96
|
+
expires_at: null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Shape check only. Delivery must additionally verify the bot's live server record. */
|
|
100
|
+
export function serverLoopTick(msg: AgentsChatMessage): ServerLoopTick | null {
|
|
101
|
+
const meta = msg.meta as Partial<ServerLoopTick> | undefined;
|
|
102
|
+
if (!meta || meta.kind !== "loop_tick" ||
|
|
103
|
+
typeof msg.id !== "string" || !msg.id || typeof msg.channel_id !== "string" || !msg.channel_id ||
|
|
104
|
+
typeof msg.sender_id !== "string" || !msg.sender_id || msg.sender_type !== "agent" || msg.content_type !== "text" ||
|
|
105
|
+
typeof msg.timestamp !== "string" || !Number.isFinite(Date.parse(msg.timestamp)) ||
|
|
106
|
+
typeof meta.loop_id !== "string" || !/^loop_[a-zA-Z0-9_-]+$/.test(meta.loop_id) ||
|
|
107
|
+
typeof meta.prompt !== "string" || !meta.prompt.trim() || meta.prompt.length > 4000 ||
|
|
108
|
+
!Number.isSafeInteger(meta.interval_ms) || meta.interval_ms! < 60_000 || meta.interval_ms! > 86_400_000 ||
|
|
109
|
+
!Number.isSafeInteger(meta.next_tick_ms) || meta.next_tick_ms! <= meta.interval_ms! || meta.expires_at !== null ||
|
|
110
|
+
msg.content !== `(loop tick — ${meta.prompt})`) return null;
|
|
111
|
+
return meta as ServerLoopTick;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The caller-scoped authenticated /api/loops/mine response is the authority. */
|
|
115
|
+
export function matchesLiveLoop(msg: AgentsChatMessage, agentId: string, response: unknown): boolean {
|
|
116
|
+
const tick = serverLoopTick(msg);
|
|
117
|
+
if (!tick || msg.sender_id !== agentId) return false;
|
|
118
|
+
const rows = (response as {loops?: any[]})?.loops;
|
|
119
|
+
if (!Array.isArray(rows)) return false;
|
|
120
|
+
const matches = rows.filter(row => row?.loop_id === tick.loop_id);
|
|
121
|
+
if (matches.length !== 1) return false;
|
|
122
|
+
const row = matches[0];
|
|
123
|
+
return row.status === "active" && row.channel_id === msg.channel_id && row.prompt === tick.prompt &&
|
|
124
|
+
row.interval_ms === tick.interval_ms && row.next_tick_ms === tick.next_tick_ms && row.expires_at === null &&
|
|
125
|
+
(row.mode === undefined || row.mode === "static") &&
|
|
126
|
+
Number.isSafeInteger(row.last_tick_at) && row.last_tick_at > 0 &&
|
|
127
|
+
row.last_tick_at + row.interval_ms === row.next_tick_ms;
|
|
128
|
+
}
|
package/connector/run.ts
CHANGED
|
@@ -413,6 +413,14 @@ const connector = startConnector({
|
|
|
413
413
|
const data = (await res.json()) as any;
|
|
414
414
|
return { id: data?.id };
|
|
415
415
|
},
|
|
416
|
+
async getLoops(botId) {
|
|
417
|
+
const id = requireIdentity(identities, botId);
|
|
418
|
+
const res = await fetch(`${API}/api/loops/mine`, {
|
|
419
|
+
headers: { Authorization: `Bearer ${id.token}` }, signal: AbortSignal.timeout(5000),
|
|
420
|
+
});
|
|
421
|
+
if (!res.ok) throw new PlatformHttpError(res.status);
|
|
422
|
+
return await res.json();
|
|
423
|
+
},
|
|
416
424
|
async getChatInfo(botId, chatId) {
|
|
417
425
|
const id = requireIdentity(identities, botId);
|
|
418
426
|
const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}`, {
|
package/connector/server.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { createServer, type Server as HttpServer, type IncomingMessage } from "n
|
|
|
15
15
|
import { WebSocketServer, WebSocket } from "ws";
|
|
16
16
|
import { verifyUpgradeToken, CLOSE_UNAUTHORIZED } from "./auth.ts";
|
|
17
17
|
import { buildDescriptor, type CapabilityDescriptor } from "./descriptor.ts";
|
|
18
|
-
import { toWireEvent, type AgentsChatMessage } from "./normalize.ts";
|
|
18
|
+
import { toWireEvent, serverLoopTick, matchesLiveLoop, type AgentsChatMessage } from "./normalize.ts";
|
|
19
19
|
import { MessageDedup } from "../src/dedup.ts";
|
|
20
20
|
import { IdentityTable, routeInboundTargets, hermesSourceProfile, type Identity } from "./identities.ts";
|
|
21
21
|
|
|
@@ -24,6 +24,8 @@ export interface AgentsChatHooks {
|
|
|
24
24
|
sendMessage(botId: string, chatId: string, content: string, replyTo?: string): Promise<{ id?: string }>;
|
|
25
25
|
getChatInfo(botId: string, chatId: string): Promise<{ name?: string; type?: string }>;
|
|
26
26
|
sendTyping?(botId: string, chatId: string): Promise<void>;
|
|
27
|
+
/** Read /api/loops/mine using this bot's own credentials; missing hook rejects self ticks. */
|
|
28
|
+
getLoops?(botId: string): Promise<unknown>;
|
|
27
29
|
/**
|
|
28
30
|
* Recent channel messages to attach as `context` when an @-mention arrives —
|
|
29
31
|
* the "what happened since the last time I was addressed" window. `sinceTs` is
|
|
@@ -293,15 +295,21 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
293
295
|
//
|
|
294
296
|
// DM: always forward. __botId (which identity's agentschat socket it arrived
|
|
295
297
|
// on) is the ownership signal; single-tenant falls back to its one identity.
|
|
296
|
-
// Group: forward
|
|
298
|
+
// Group: ordinary messages forward only when they @mention a fronted identity (content-
|
|
297
299
|
// based — the agentschat WS pushes every message of a joined channel
|
|
298
300
|
// unannotated, and arrival on a socket is NOT an addressing signal). The
|
|
299
301
|
// MCP path's gate is isDM || isMentioned; this reproduces it. Anything
|
|
300
302
|
// unaddressed is dropped — injecting joined-channel chatter into the
|
|
301
303
|
// agent's session would burn its tokens on messages not meant for it.
|
|
304
|
+
// Own server loop ticks bypass those gates only after live-record verification.
|
|
302
305
|
const isDm = typeof msg.channel_id === "string" && msg.channel_id.startsWith("dm-");
|
|
303
306
|
const owner = (msg as any).__botId ?? (msg as any).dm_owner;
|
|
304
|
-
const
|
|
307
|
+
const isTick = (msg.meta as any)?.kind === "loop_tick";
|
|
308
|
+
const tick = serverLoopTick(msg);
|
|
309
|
+
const loopTargets = tick ? table.all().filter(id => id.agentId === msg.sender_id) : [];
|
|
310
|
+
// A server loop addresses only its own identity, even if the prompt @mentions others.
|
|
311
|
+
// Never interpret a malformed/foreign loop as an ordinary mention or DM.
|
|
312
|
+
const targets = isTick ? (loopTargets.length === 1 ? loopTargets : []) : isDm
|
|
305
313
|
? [owner !== undefined ? (typeof owner === "string" ? table.forBot(owner) : null)
|
|
306
314
|
: table.isSingle() ? table.all()[0] : null].filter((id): id is Identity => !!id)
|
|
307
315
|
: routeInboundTargets(table, {
|
|
@@ -317,7 +325,14 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
317
325
|
const key = JSON.stringify([target.botId, msg.channel_id]);
|
|
318
326
|
const previous = inboundQueues.get(key) ?? Promise.resolve();
|
|
319
327
|
const job = previous.catch(() => {}).then(async () => {
|
|
320
|
-
if (
|
|
328
|
+
if (tick) {
|
|
329
|
+
let verified = false;
|
|
330
|
+
try { verified = !!hooks.getLoops && matchesLiveLoop(msg, target.agentId, await hooks.getLoops(target.botId)); } catch { /* fail closed */ }
|
|
331
|
+
if (!verified || table.forBot(target.botId) !== target) {
|
|
332
|
+
log("[connector] loop tick rejected: current bot loop could not be verified");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
} else if (msg.sender_id === target.agentId) return;
|
|
321
336
|
const baseEvent = toWireEvent(msg, "agentschat");
|
|
322
337
|
if (!baseEvent) return;
|
|
323
338
|
if (![...sockets].some(c => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId))) return;
|
|
@@ -325,6 +340,8 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
325
340
|
? JSON.stringify([msg.channel_id, msg.id, target.botId]) : null;
|
|
326
341
|
// Reserve synchronously, before async context fetch, across mirrored sockets.
|
|
327
342
|
if (deliveryKey && delivered.recordOrSkip(deliveryKey)) return;
|
|
343
|
+
// A mirrored/replayed tick must not run twice even if its envelope ID changes.
|
|
344
|
+
if (tick && delivered.recordOrSkip(JSON.stringify(["loop_tick", target.botId, msg.channel_id, tick.loop_id, tick.next_tick_ms]))) return;
|
|
328
345
|
if (!isDm) {
|
|
329
346
|
// Attach "what happened since you were last addressed" so the agent gets
|
|
330
347
|
// the conversation BETWEEN its @-mentions without being injected into
|
|
@@ -355,6 +372,12 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
355
372
|
if (msg.timestamp && Number.isFinite(Date.parse(msg.timestamp)) &&
|
|
356
373
|
(!since || Date.parse(msg.timestamp) > Date.parse(since))) lastAddressed.set(key, msg.timestamp);
|
|
357
374
|
}
|
|
375
|
+
if (tick) {
|
|
376
|
+
baseEvent.context = [...(baseEvent.context ?? []), {
|
|
377
|
+
text: `AgentsChat runtime context: this is your verified scheduled task in channel ${msg.channel_id}, running as ${target.agentId}. Execute it once in this same conversation; replies stay here. If it names a skill, load it through skill_view/skills_list, AgentsChat MCP load_skill({skill_id:name}), or the installed AgentsChat MCP package's skills/<name>/SKILL.md in this runtime. Missing skill or tools is a failure to report, not successful execution. Do not repeat the skill text or create another loop.`,
|
|
378
|
+
source: {user_name: "AgentsChat runtime"},
|
|
379
|
+
}];
|
|
380
|
+
}
|
|
358
381
|
const deliverTo = [...sockets].filter((c) => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId)).slice(0, 1);
|
|
359
382
|
if (deliverTo.length === 0) {
|
|
360
383
|
log(`[connector] inbound dropped for botId=${target.botId}: no agentschat-fronted gateway socket`);
|
package/dist/codex-bots.js
CHANGED
|
@@ -904,6 +904,13 @@ function parse(toml, { maxDepth = 1000, integersAsBigInt } = {}) {
|
|
|
904
904
|
*/
|
|
905
905
|
|
|
906
906
|
// codex/config.ts
|
|
907
|
+
function reasoningEffort(value) {
|
|
908
|
+
if (value === undefined)
|
|
909
|
+
return;
|
|
910
|
+
if (typeof value !== "string" || !value.trim())
|
|
911
|
+
throw new Error("effort must be a nonempty model-supported reasoning effort name");
|
|
912
|
+
return value.trim();
|
|
913
|
+
}
|
|
907
914
|
function permissionMode(value) {
|
|
908
915
|
if (value === undefined)
|
|
909
916
|
return "full-access";
|
|
@@ -931,7 +938,7 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
|
|
|
931
938
|
const project = opts.settings ?? (existsSync(configFile) ? readJson(configFile) : {});
|
|
932
939
|
if (!project || typeof project !== "object" || Array.isArray(project))
|
|
933
940
|
throw new Error("Invalid project config");
|
|
934
|
-
const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions"]);
|
|
941
|
+
const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"]);
|
|
935
942
|
if (Object.keys(project).some((k) => !allowed.has(k)))
|
|
936
943
|
throw new Error("Unknown project config field (credentials belong in a private profile)");
|
|
937
944
|
for (const k of ["profile", "agent_id", "api_url", "ws_url"])
|
|
@@ -1001,6 +1008,7 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
|
|
|
1001
1008
|
profileFile,
|
|
1002
1009
|
source,
|
|
1003
1010
|
permissions: permissionMode(project.permissions),
|
|
1011
|
+
effort: reasoningEffort(project.effort),
|
|
1004
1012
|
agentId: profile.agent_id,
|
|
1005
1013
|
token: profile.token,
|
|
1006
1014
|
apiUrl: canonicalApi,
|
|
@@ -1040,7 +1048,7 @@ function loadBots(file = defaultRegistry(), home = homedir2()) {
|
|
|
1040
1048
|
const names = new Set, identities = new Set;
|
|
1041
1049
|
const bots = [];
|
|
1042
1050
|
for (const bot of doc.bots) {
|
|
1043
|
-
fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions"]);
|
|
1051
|
+
fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"]);
|
|
1044
1052
|
if (!text(bot.name) || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(bot.name) || names.has(bot.name))
|
|
1045
1053
|
throw new Error("Bot names must be unique simple labels");
|
|
1046
1054
|
names.add(bot.name);
|
|
@@ -1056,7 +1064,7 @@ function loadBots(file = defaultRegistry(), home = homedir2()) {
|
|
|
1056
1064
|
mkdirSync(defaultDir, { recursive: true, mode: 448 });
|
|
1057
1065
|
const cwd = realpathSync2(bot.workdir ? path(bot.workdir) : defaultDir);
|
|
1058
1066
|
const settings = {};
|
|
1059
|
-
for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url", "permissions"])
|
|
1067
|
+
for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"])
|
|
1060
1068
|
if (bot[k] !== undefined)
|
|
1061
1069
|
settings[k] = bot[k];
|
|
1062
1070
|
const config = resolveConfig({ cwd, profile: bot.profile, settings, codexBin: doc.codex_bin }, {}, home);
|
|
@@ -1122,7 +1130,7 @@ function removeDeadLock(file) {
|
|
|
1122
1130
|
}
|
|
1123
1131
|
}
|
|
1124
1132
|
function save() {
|
|
1125
|
-
writeFileSync(statusFile + ".tmp", JSON.stringify({ pid: process.pid, updated_at: new Date().toISOString(), bots: [...workers.values()].map((w) => ({ name: w.config.name, agent_id: w.config.agentId, workdir: w.config.cwd, pid: w.child?.pid, status: w.status })) }, null, 2), { mode: 384 });
|
|
1133
|
+
writeFileSync(statusFile + ".tmp", JSON.stringify({ pid: process.pid, updated_at: new Date().toISOString(), bots: [...workers.values()].map((w) => ({ name: w.config.name, agent_id: w.config.agentId, workdir: w.config.cwd, effort: w.config.effort, pid: w.child?.pid, status: w.status })) }, null, 2), { mode: 384 });
|
|
1126
1134
|
renameSync(statusFile + ".tmp", statusFile);
|
|
1127
1135
|
}
|
|
1128
1136
|
async function stop(w) {
|