@masons/agent-network 0.4.20 → 0.4.22
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/channel.d.ts +2 -0
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +42 -52
- package/dist/config.d.ts +17 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +35 -48
- package/dist/conversation-manager.d.ts +44 -6
- package/dist/conversation-manager.d.ts.map +1 -1
- package/dist/conversation-manager.js +51 -9
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +44 -23
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +71 -48
- package/dist/turn-context.d.ts +22 -0
- package/dist/turn-context.d.ts.map +1 -1
- package/dist/turn-context.js +34 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/agent-network/SKILL.md +30 -17
package/dist/channel.d.ts
CHANGED
package/dist/channel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AA2B9D,oEAAoE;AACpE,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAEvD;AAQD,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,oBAAoB,CAAC,CAAC;IAC9B,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC;IACvD,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IACpE,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IACjE,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;CAC/D;AAED,UAAU,sBAAsB;IAC9B,EAAE,EAAE,OAAO,CAAC;CACb;AAED,UAAU,sBAAsB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,sBAAsB;IAC9B,YAAY,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9C,QAAQ,CAAC,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzE;AAED,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAsDD,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,YAAY,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/D,WAAW,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,UAAU,yBAAyB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,mBAAmB,CAAC;IAClC,MAAM,EAAE,oBAAoB,CAAC,mBAAmB,CAAC,CAAC;IAClD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,OAAO,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAC;CACrD;AAgQD,eAAO,MAAM,mBAAmB,EAAE,yBAwNjC,CAAC"}
|
package/dist/channel.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import createDebug from "debug";
|
|
3
|
-
import { clearConnectorClient, clearConversationManager, extractNetworkConfig,
|
|
3
|
+
import { clearConnectorClient, clearConversationManager, extractNetworkConfig, getDmScope, initConnectorClient, initConversationManager, initToolConfig, isIdentityLinksConfigured, requirePluginRuntime, } from "./config.js";
|
|
4
4
|
import { ConnectorClient } from "./connector-client.js";
|
|
5
5
|
import { ConversationManager } from "./conversation-manager.js";
|
|
6
|
-
import { clearEnvironmentContext
|
|
6
|
+
import { clearEnvironmentContext } from "./environment-context.js";
|
|
7
7
|
import { extractHandleFromAddress } from "./handle-utils.js";
|
|
8
8
|
import { ownerNotesQueue } from "./owner-notes.js";
|
|
9
9
|
import { clearOwnerState, isOwnerAddress, markOwnerAddress, setCurrentTurnIsOwner, } from "./owner-session-state.js";
|
|
10
10
|
import { setCurrentTurnSender } from "./turn-context.js";
|
|
11
11
|
import { checkForUpdate } from "./update-check.js";
|
|
12
12
|
const dbg = createDebug("agent-network:channel");
|
|
13
|
-
// Cross-channel identity
|
|
14
|
-
//
|
|
13
|
+
// Cross-channel identity notification: once per startAccount lifecycle.
|
|
14
|
+
// Reset on startAccount(). isIdentityLinksConfigured() provides the
|
|
15
15
|
// cross-restart idempotency (checks config file).
|
|
16
|
-
let
|
|
16
|
+
let notificationAttempted = false;
|
|
17
|
+
// Owner's Passport address: recorded on first is_owner MESSAGE_RECEIVED.
|
|
18
|
+
// Used by masons_link_identity tool to auto-include the Passport entry.
|
|
19
|
+
let storedOwnerPassportAddress = null;
|
|
20
|
+
/** Get the stored owner Passport address (first is_owner visit). */
|
|
21
|
+
export function getOwnerPassportAddress() {
|
|
22
|
+
return storedOwnerPassportAddress;
|
|
23
|
+
}
|
|
17
24
|
// --- Sender identity ---
|
|
18
25
|
/**
|
|
19
26
|
* Extract a human-readable name from the sender's metadata.
|
|
@@ -86,52 +93,29 @@ async function handleAddressedMessage(event, conversationManager, client, ctx) {
|
|
|
86
93
|
// Connector includes is_owner in metadata for Passport visitors.
|
|
87
94
|
if (event.metadata?.is_owner === true) {
|
|
88
95
|
markOwnerAddress(fromAddress);
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// -
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// -
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// TODO: i18n — notification language should match owner's locale
|
|
113
|
-
ownerNotesQueue.enqueue({
|
|
114
|
-
content: "你刚才在我的网页上跟我聊过。我已经准备好了跨渠道关联 — " +
|
|
115
|
-
"重启后不管你从 Telegram 还是网页跟我对话,我都会记得之前聊过什么。" +
|
|
116
|
-
"如果这不是你,告诉我撤销。",
|
|
117
|
-
timestamp: Date.now(),
|
|
118
|
-
});
|
|
119
|
-
dbg("auto-linked owner identity: %s → telegram:%s + agent-network:%s", ownerHandle, telegramId, fromAddress);
|
|
120
|
-
})
|
|
121
|
-
.catch((err) => {
|
|
122
|
-
dbg("auto-link writeIdentityLinks failed: %O", err);
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
else if (ownerHandle) {
|
|
126
|
-
// Scenario B: cannot determine Telegram ID — notify owner.
|
|
127
|
-
// TODO: i18n — notification language should match owner's locale
|
|
128
|
-
ownerNotesQueue.enqueue({
|
|
129
|
-
content: "你刚才在我的网页上跟我聊过。如果你想让我在 Telegram 和网页上" +
|
|
130
|
-
"都能认出你、记住对话,告诉我你的 Telegram 用户 ID。",
|
|
131
|
-
timestamp: Date.now(),
|
|
132
|
-
});
|
|
133
|
-
dbg("owner detected on Passport but no Telegram ID available");
|
|
134
|
-
}
|
|
96
|
+
// Record Passport address for tool auto-include (first visit only)
|
|
97
|
+
if (!storedOwnerPassportAddress) {
|
|
98
|
+
storedOwnerPassportAddress = fromAddress;
|
|
99
|
+
dbg("recorded owner Passport address: %s", fromAddress);
|
|
100
|
+
}
|
|
101
|
+
// --- dmScope-aware notification (2026-03-26, #986 redesign) ---
|
|
102
|
+
// Notify the owner about cross-channel identity linking availability.
|
|
103
|
+
// Notification is:
|
|
104
|
+
// - Skipped in "main" mode (linking has no effect — Gateway skips resolveLinkedPeerId)
|
|
105
|
+
// - Skipped if identityLinks already configured (already linked)
|
|
106
|
+
// - Sent once per startAccount lifecycle (in-memory idempotency)
|
|
107
|
+
// Content is channel-agnostic and language-neutral (semantic event, not identity claim).
|
|
108
|
+
if (getDmScope() !== "main" &&
|
|
109
|
+
!isIdentityLinksConfigured() &&
|
|
110
|
+
!notificationAttempted) {
|
|
111
|
+
notificationAttempted = true;
|
|
112
|
+
ownerNotesQueue.enqueue({
|
|
113
|
+
content: "Owner visited via Passport (verified). " +
|
|
114
|
+
"Cross-channel identity linking is available but not configured. " +
|
|
115
|
+
"The owner can link their identity across channels so conversations share context.",
|
|
116
|
+
timestamp: Date.now(),
|
|
117
|
+
});
|
|
118
|
+
dbg("queued identity linking notification (dmScope=%s)", getDmScope());
|
|
135
119
|
}
|
|
136
120
|
}
|
|
137
121
|
setCurrentTurnIsOwner(isOwnerAddress(fromAddress));
|
|
@@ -220,6 +204,10 @@ async function handleAddressedMessage(event, conversationManager, client, ctx) {
|
|
|
220
204
|
}
|
|
221
205
|
finally {
|
|
222
206
|
client.sendTyping(fromAddress, false);
|
|
207
|
+
// Commit deferred deletions from end_conversation calls made during
|
|
208
|
+
// this dispatch cycle. Must run after text buffer flush so that
|
|
209
|
+
// hasConversation() returns true for the deliver callback (#999).
|
|
210
|
+
conversationManager.commitEndedConversations();
|
|
223
211
|
}
|
|
224
212
|
}
|
|
225
213
|
catch (err) {
|
|
@@ -298,7 +286,9 @@ export const agentNetworkChannel = {
|
|
|
298
286
|
gateway: {
|
|
299
287
|
async startAccount(ctx) {
|
|
300
288
|
dbg("starting account %s", ctx.accountId);
|
|
301
|
-
|
|
289
|
+
// Reset per-session state
|
|
290
|
+
notificationAttempted = false;
|
|
291
|
+
storedOwnerPassportAddress = null;
|
|
302
292
|
// Defensive: clear stale singletons from a previous cycle.
|
|
303
293
|
// The abort handler intentionally leaves them alive for in-flight tool
|
|
304
294
|
// calls (#935). Clean up here at cycle start so initConnectorClient()
|
package/dist/config.d.ts
CHANGED
|
@@ -44,8 +44,16 @@ export declare function initToolConfig(cfg: Record<string, unknown>): void;
|
|
|
44
44
|
* identity is linked to another channel.
|
|
45
45
|
*/
|
|
46
46
|
export declare function isIdentityLinksConfigured(): boolean;
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Current dmScope mode from Gateway config (`cfg.session.dmScope`).
|
|
49
|
+
*
|
|
50
|
+
* Determines how the plugin handles cross-channel identity:
|
|
51
|
+
* - "main" (default): all DMs share one session. Identity linking has no effect.
|
|
52
|
+
* - "per-peer": identity linking merges sessions across channels.
|
|
53
|
+
* - "per-channel-peer" / "per-account-channel-peer": identity linking normalizes
|
|
54
|
+
* peerId but sessions stay separate per channel.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getDmScope(): string;
|
|
49
57
|
/**
|
|
50
58
|
* Inject the live ConnectorClient after WebSocket connection is established.
|
|
51
59
|
*
|
|
@@ -194,8 +202,13 @@ export declare function markProfileComplete(): Promise<void>;
|
|
|
194
202
|
/**
|
|
195
203
|
* Write cross-channel identity links to `config.session.identityLinks`.
|
|
196
204
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
205
|
+
* **Entry-level additive merge** (2026-03-26, #986 redesign):
|
|
206
|
+
* New entries are merged INTO the existing canonical group, deduped,
|
|
207
|
+
* lowercase-normalized. Never overwrites other canonical groups, and
|
|
208
|
+
* never removes existing entries within the same group.
|
|
209
|
+
*
|
|
210
|
+
* This allows one-channel-at-a-time linking: first call adds Telegram,
|
|
211
|
+
* second call adds Feishu — without losing the Telegram entry.
|
|
199
212
|
*
|
|
200
213
|
* This writes to `session.*` (Gateway-level), not `channels.agent-network.*`.
|
|
201
214
|
* Justified because cross-channel identity is inherently a Gateway concern,
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/config.js
CHANGED
|
@@ -31,9 +31,14 @@ let platformConfig = { apiHost: DEFAULT_API_HOST };
|
|
|
31
31
|
let storedApiKey = null;
|
|
32
32
|
let storedPendingTarget = null;
|
|
33
33
|
let storedNeedsProfile = false;
|
|
34
|
-
// Cross-channel identity state (from cfg.session
|
|
34
|
+
// Cross-channel identity state (from cfg.session — set by initToolConfig)
|
|
35
35
|
let storedIdentityLinksConfigured = false;
|
|
36
|
-
|
|
36
|
+
// dmScope from cfg.session.dmScope — determines notification + linking behavior.
|
|
37
|
+
// "main" (default): all DMs share one session. Identity linking has no effect.
|
|
38
|
+
// "per-peer": identity linking merges sessions across channels.
|
|
39
|
+
// "per-channel-peer" / "per-account-channel-peer": identity linking normalizes
|
|
40
|
+
// peerId but sessions stay separate per channel.
|
|
41
|
+
let storedDmScope = "main";
|
|
37
42
|
// State cache generation — incremented when config-modifying functions write
|
|
38
43
|
// to disk (writeCredentials, clearTargetHandle, markProfileNeeded, markProfileComplete).
|
|
39
44
|
// before_prompt_build compares its cached generation to this value; mismatch
|
|
@@ -99,19 +104,20 @@ export function initToolConfig(cfg) {
|
|
|
99
104
|
: null;
|
|
100
105
|
storedNeedsProfile = networkCfg?.needsProfile === true;
|
|
101
106
|
// --- Cross-channel identity state (cfg.session, not cfg.channels) ---
|
|
102
|
-
// Decision: 2026-03-26, during cross-channel identity linking design (#969).
|
|
107
|
+
// Decision: 2026-03-26, during cross-channel identity linking design (#969, #986).
|
|
103
108
|
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
// channel.ts auto-link logic and tools.
|
|
109
|
+
// Reads Gateway-level session config to detect:
|
|
110
|
+
// 1. Whether identityLinks already contains agent-network entries
|
|
111
|
+
// 2. The dmScope mode (determines notification + linking behavior)
|
|
108
112
|
//
|
|
109
113
|
// These are NOT agent-network-specific — any channel plugin would need
|
|
110
114
|
// similar capabilities. They live here because only one channel plugin
|
|
111
115
|
// exists today. When OpenClaw provides native cross-channel identity
|
|
112
116
|
// management, this should migrate.
|
|
113
117
|
storedIdentityLinksConfigured = hasAgentNetworkIdentityLink(cfg);
|
|
114
|
-
|
|
118
|
+
const sessionCfg = cfg.session;
|
|
119
|
+
storedDmScope =
|
|
120
|
+
typeof sessionCfg?.dmScope === "string" ? sessionCfg.dmScope : "main";
|
|
115
121
|
}
|
|
116
122
|
// ---------------------------------------------------------------------------
|
|
117
123
|
// Cross-channel identity detection
|
|
@@ -149,44 +155,16 @@ export function isIdentityLinksConfigured() {
|
|
|
149
155
|
return storedIdentityLinksConfigured;
|
|
150
156
|
}
|
|
151
157
|
/**
|
|
152
|
-
*
|
|
158
|
+
* Current dmScope mode from Gateway config (`cfg.session.dmScope`).
|
|
153
159
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* inferred from the single-user assumption (allowFrom has exactly 1 entry =
|
|
160
|
-
* that person is the owner by convention).
|
|
161
|
-
*
|
|
162
|
-
* Platform-gap workaround (2026-03-26, #969): reads another channel's config
|
|
163
|
-
* (cfg.channels.telegram) to infer the owner's Telegram ID. When OpenClaw
|
|
164
|
-
* provides a native "owner identifiers" API, this cross-channel read should
|
|
165
|
-
* be removed.
|
|
166
|
-
*
|
|
167
|
-
* Strips `telegram:` / `tg:` prefix if present (OpenClaw normalizes these).
|
|
160
|
+
* Determines how the plugin handles cross-channel identity:
|
|
161
|
+
* - "main" (default): all DMs share one session. Identity linking has no effect.
|
|
162
|
+
* - "per-peer": identity linking merges sessions across channels.
|
|
163
|
+
* - "per-channel-peer" / "per-account-channel-peer": identity linking normalizes
|
|
164
|
+
* peerId but sessions stay separate per channel.
|
|
168
165
|
*/
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
if (!channels)
|
|
172
|
-
return null;
|
|
173
|
-
const telegram = channels.telegram;
|
|
174
|
-
if (!telegram)
|
|
175
|
-
return null;
|
|
176
|
-
const allowFrom = telegram.allowFrom;
|
|
177
|
-
if (!Array.isArray(allowFrom) || allowFrom.length !== 1)
|
|
178
|
-
return null;
|
|
179
|
-
const raw = String(allowFrom[0] ?? "")
|
|
180
|
-
.replace(/^(telegram|tg):/i, "")
|
|
181
|
-
.trim();
|
|
182
|
-
// Validate: must be numeric, 1-20 digits
|
|
183
|
-
if (/^\d{1,20}$/.test(raw))
|
|
184
|
-
return raw;
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
/** Telegram owner user ID (only when allowFrom has exactly 1 entry). */
|
|
188
|
-
export function getTelegramOwnerUserId() {
|
|
189
|
-
return storedTelegramOwnerUserId;
|
|
166
|
+
export function getDmScope() {
|
|
167
|
+
return storedDmScope;
|
|
190
168
|
}
|
|
191
169
|
// ---------------------------------------------------------------------------
|
|
192
170
|
// ConnectorClient injection (called by startAccount() after connect)
|
|
@@ -496,8 +474,13 @@ function ensureSessionSection(config) {
|
|
|
496
474
|
/**
|
|
497
475
|
* Write cross-channel identity links to `config.session.identityLinks`.
|
|
498
476
|
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
477
|
+
* **Entry-level additive merge** (2026-03-26, #986 redesign):
|
|
478
|
+
* New entries are merged INTO the existing canonical group, deduped,
|
|
479
|
+
* lowercase-normalized. Never overwrites other canonical groups, and
|
|
480
|
+
* never removes existing entries within the same group.
|
|
481
|
+
*
|
|
482
|
+
* This allows one-channel-at-a-time linking: first call adds Telegram,
|
|
483
|
+
* second call adds Feishu — without losing the Telegram entry.
|
|
501
484
|
*
|
|
502
485
|
* This writes to `session.*` (Gateway-level), not `channels.agent-network.*`.
|
|
503
486
|
* Justified because cross-channel identity is inherently a Gateway concern,
|
|
@@ -513,7 +496,11 @@ export async function writeIdentityLinks(canonical, entries) {
|
|
|
513
496
|
const existing = typeof session.identityLinks === "object" && session.identityLinks !== null
|
|
514
497
|
? session.identityLinks
|
|
515
498
|
: {};
|
|
516
|
-
|
|
499
|
+
// Entry-level additive merge: combine new entries into existing group
|
|
500
|
+
const current = existing[canonical] ?? [];
|
|
501
|
+
const normalized = entries.map((e) => e.toLowerCase());
|
|
502
|
+
const merged = [...new Set([...current, ...normalized])];
|
|
503
|
+
existing[canonical] = merged;
|
|
517
504
|
session.identityLinks = existing;
|
|
518
505
|
await persistConfig(config);
|
|
519
506
|
// Update module-level state — same exception pattern as writeCredentials
|
|
@@ -582,7 +569,7 @@ export function _resetForTesting() {
|
|
|
582
569
|
storedPendingTarget = null;
|
|
583
570
|
storedNeedsProfile = false;
|
|
584
571
|
storedIdentityLinksConfigured = false;
|
|
585
|
-
|
|
572
|
+
storedDmScope = "main";
|
|
586
573
|
storedConnectorClient = null;
|
|
587
574
|
storedConversationManager = null;
|
|
588
575
|
storedPluginRuntime = null;
|
|
@@ -23,6 +23,20 @@ export interface ConversationEntry {
|
|
|
23
23
|
address: string;
|
|
24
24
|
lastMessageAt: number;
|
|
25
25
|
initiatedBy: "local" | "remote";
|
|
26
|
+
/**
|
|
27
|
+
* Deferred deletion flag (#999).
|
|
28
|
+
*
|
|
29
|
+
* When `masons_end_conversation` runs during a dispatch cycle, the entry
|
|
30
|
+
* is marked `ended` but NOT removed from the Map. This keeps
|
|
31
|
+
* `hasConversation()` returning true so the deliver callback can still
|
|
32
|
+
* flush buffered text from the same LLM turn.
|
|
33
|
+
*
|
|
34
|
+
* Call `commitEndedConversations()` after the dispatch cycle completes
|
|
35
|
+
* to actually remove ended entries.
|
|
36
|
+
*
|
|
37
|
+
* @see https://github.com/MASONS-ai/masons.ai/issues/999
|
|
38
|
+
*/
|
|
39
|
+
ended?: boolean;
|
|
26
40
|
}
|
|
27
41
|
export interface ConversationSummary {
|
|
28
42
|
contact: string;
|
|
@@ -68,17 +82,41 @@ export declare class ConversationManager {
|
|
|
68
82
|
/**
|
|
69
83
|
* Check if a conversation entry exists for a contact.
|
|
70
84
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
85
|
+
* Returns true for entries that are marked `ended` but not yet committed —
|
|
86
|
+
* this allows the deliver callback to flush buffered text from the same
|
|
87
|
+
* dispatch cycle as end_conversation (#999).
|
|
88
|
+
*
|
|
89
|
+
* Straggler text from *subsequent* dispatch cycles is still suppressed
|
|
90
|
+
* because `commitEndedConversations()` removes entries between cycles.
|
|
74
91
|
*/
|
|
75
92
|
hasConversation(contact: string): boolean;
|
|
76
93
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
94
|
+
* Mark the conversation with a contact as ended (deferred deletion).
|
|
95
|
+
*
|
|
96
|
+
* The entry is NOT removed immediately — it is flagged `ended` and stays
|
|
97
|
+
* in the Map until `commitEndedConversations()` is called after the
|
|
98
|
+
* current dispatch cycle completes. This prevents the "last-message-
|
|
99
|
+
* before-close" race where the dispatcher executes end_conversation
|
|
100
|
+
* before flushing buffered text (#999).
|
|
101
|
+
*
|
|
102
|
+
* No END_SESSION needed — the Connector manages connection lifecycle
|
|
103
|
+
* (idle timeout).
|
|
80
104
|
*/
|
|
81
105
|
endConversation(contact: string): void;
|
|
106
|
+
/**
|
|
107
|
+
* Remove all conversations marked as `ended`.
|
|
108
|
+
*
|
|
109
|
+
* Call this after the dispatch cycle completes (in the `finally` block
|
|
110
|
+
* of `handleAddressedMessage`). Deferred deletion ensures that text
|
|
111
|
+
* buffered in the same dispatch cycle as end_conversation is delivered
|
|
112
|
+
* before the entry disappears.
|
|
113
|
+
*
|
|
114
|
+
* Pattern: React 18 batched updates / Qt `deleteLater()` — mutations
|
|
115
|
+
* during a dispatch cycle are committed only when the cycle ends.
|
|
116
|
+
*
|
|
117
|
+
* @see https://github.com/MASONS-ai/masons.ai/issues/999
|
|
118
|
+
*/
|
|
119
|
+
commitEndedConversations(): void;
|
|
82
120
|
/**
|
|
83
121
|
* Resolve a contact (handle, MSTP address, or passport address) to a
|
|
84
122
|
* routable address.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-manager.d.ts","sourceRoot":"","sources":["../src/conversation-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,uBAAuB,CAAC;AAS7E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,GAAG,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"conversation-manager.d.ts","sourceRoot":"","sources":["../src/conversation-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,uBAAuB,CAAC;AAS7E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,GAAG,QAAQ,CAAC;IAChC;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,GAAG,QAAQ,CAAC;CACjC;AAwBD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,8CAA8C;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;gBAE1D,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM;IAS1D;;;;;;;;;;OAUG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA0CjE;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAKvD;;;OAGG;IACH,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAaxD;;;;OAIG;IACH,iBAAiB,IAAI,mBAAmB,EAAE;IAc1C;;;;;;;;;OASG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAKzC;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAWtC;;;;;;;;;;;;OAYG;IACH,wBAAwB,IAAI,IAAI;IAiBhC;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAoBvC;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAQtC,OAAO,CAAC,uBAAuB;IAyB/B,0CAA0C;IAC1C,gBAAgB,IAAI,IAAI;CAGzB"}
|
|
@@ -122,7 +122,7 @@ export class ConversationManager {
|
|
|
122
122
|
result.push({
|
|
123
123
|
contact: entry.contact,
|
|
124
124
|
address: entry.address,
|
|
125
|
-
active:
|
|
125
|
+
active: !entry.ended,
|
|
126
126
|
lastMessageAt: entry.lastMessageAt,
|
|
127
127
|
initiatedBy: entry.initiatedBy,
|
|
128
128
|
});
|
|
@@ -132,23 +132,64 @@ export class ConversationManager {
|
|
|
132
132
|
/**
|
|
133
133
|
* Check if a conversation entry exists for a contact.
|
|
134
134
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
135
|
+
* Returns true for entries that are marked `ended` but not yet committed —
|
|
136
|
+
* this allows the deliver callback to flush buffered text from the same
|
|
137
|
+
* dispatch cycle as end_conversation (#999).
|
|
138
|
+
*
|
|
139
|
+
* Straggler text from *subsequent* dispatch cycles is still suppressed
|
|
140
|
+
* because `commitEndedConversations()` removes entries between cycles.
|
|
138
141
|
*/
|
|
139
142
|
hasConversation(contact) {
|
|
140
143
|
const address = this.resolveAddress(contact);
|
|
141
144
|
return this.conversations.has(address);
|
|
142
145
|
}
|
|
143
146
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
+
* Mark the conversation with a contact as ended (deferred deletion).
|
|
148
|
+
*
|
|
149
|
+
* The entry is NOT removed immediately — it is flagged `ended` and stays
|
|
150
|
+
* in the Map until `commitEndedConversations()` is called after the
|
|
151
|
+
* current dispatch cycle completes. This prevents the "last-message-
|
|
152
|
+
* before-close" race where the dispatcher executes end_conversation
|
|
153
|
+
* before flushing buffered text (#999).
|
|
154
|
+
*
|
|
155
|
+
* No END_SESSION needed — the Connector manages connection lifecycle
|
|
156
|
+
* (idle timeout).
|
|
147
157
|
*/
|
|
148
158
|
endConversation(contact) {
|
|
149
159
|
const address = this.resolveAddress(contact);
|
|
150
|
-
this.conversations.
|
|
151
|
-
|
|
160
|
+
const entry = this.conversations.get(address);
|
|
161
|
+
if (entry) {
|
|
162
|
+
entry.ended = true;
|
|
163
|
+
dbg("endConversation (deferred) contact=%s address=%s", contact, address);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
dbg("endConversation (no-op) contact=%s address=%s", contact, address);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Remove all conversations marked as `ended`.
|
|
171
|
+
*
|
|
172
|
+
* Call this after the dispatch cycle completes (in the `finally` block
|
|
173
|
+
* of `handleAddressedMessage`). Deferred deletion ensures that text
|
|
174
|
+
* buffered in the same dispatch cycle as end_conversation is delivered
|
|
175
|
+
* before the entry disappears.
|
|
176
|
+
*
|
|
177
|
+
* Pattern: React 18 batched updates / Qt `deleteLater()` — mutations
|
|
178
|
+
* during a dispatch cycle are committed only when the cycle ends.
|
|
179
|
+
*
|
|
180
|
+
* @see https://github.com/MASONS-ai/masons.ai/issues/999
|
|
181
|
+
*/
|
|
182
|
+
commitEndedConversations() {
|
|
183
|
+
let count = 0;
|
|
184
|
+
for (const [address, entry] of this.conversations) {
|
|
185
|
+
if (entry.ended) {
|
|
186
|
+
this.conversations.delete(address);
|
|
187
|
+
count++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (count > 0) {
|
|
191
|
+
dbg("commitEndedConversations removed=%d", count);
|
|
192
|
+
}
|
|
152
193
|
}
|
|
153
194
|
// -------------------------------------------------------------------------
|
|
154
195
|
// Contact resolution
|
|
@@ -210,6 +251,7 @@ export class ConversationManager {
|
|
|
210
251
|
const entry = this.conversations.get(address);
|
|
211
252
|
if (entry) {
|
|
212
253
|
entry.lastMessageAt = Date.now();
|
|
254
|
+
entry.ended = false; // Clear deferred deletion on re-registration
|
|
213
255
|
}
|
|
214
256
|
}
|
|
215
257
|
}
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AA0HA,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI,CAAC;CACnE;AAED,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;kBA4BI,iBAAiB;CA8VhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/plugin.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// NOT imported by index.ts to avoid pulling ws/typebox into Next.js app bundles.
|
|
4
4
|
import { agentNetworkChannel } from "./channel.js";
|
|
5
5
|
import { configureInteractive } from "./cli-setup.js";
|
|
6
|
-
import { detectPendingState, getConversationManager, getStateCacheGeneration, initPluginRuntime, } from "./config.js";
|
|
6
|
+
import { detectPendingState, getConversationManager, getDmScope, getStateCacheGeneration, initPluginRuntime, } from "./config.js";
|
|
7
7
|
import { getAgentIdentity } from "./environment-context.js";
|
|
8
8
|
import { ownerNotesQueue } from "./owner-notes.js";
|
|
9
9
|
import { consumeCurrentTurnIsOwner } from "./owner-session-state.js";
|
|
10
10
|
import { sentMessageBuffer } from "./sent-message-buffer.js";
|
|
11
11
|
import { registerTools } from "./tools.js";
|
|
12
|
-
import { consumeCurrentTurnSender } from "./turn-context.js";
|
|
12
|
+
import { consumeCurrentTurnSender, setCurrentTurnChannelId, setCurrentTurnIsOwnerForTools, } from "./turn-context.js";
|
|
13
13
|
import { getUpdateInfo } from "./update-check.js";
|
|
14
14
|
import { PLUGIN_VERSION } from "./version.js";
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
@@ -197,16 +197,29 @@ const plugin = {
|
|
|
197
197
|
"Should you follow up with another agent via masons_send_message? " +
|
|
198
198
|
"When the interaction is complete, call masons_end_conversation as your FINAL action — " +
|
|
199
199
|
"do not generate any text reply after ending. " +
|
|
200
|
-
// --- Cross-channel identity linking knowledge
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
"
|
|
205
|
-
"If your owner asks about cross-channel identity, explain how it works. " +
|
|
206
|
-
"Use masons_link_identity to link or masons_unlink_identity to undo.";
|
|
200
|
+
// --- Cross-channel identity linking knowledge removed from static TOOL_CONTEXT ---
|
|
201
|
+
// Moved to dynamicContext in before_prompt_build (#986 redesign).
|
|
202
|
+
// Reason: TOOL_CONTEXT is built in register() before startAccount() runs,
|
|
203
|
+
// so getDmScope() is not available. Linking knowledge is dmScope-conditional.
|
|
204
|
+
"";
|
|
207
205
|
api.on("before_prompt_build", async (_event, ctx) => {
|
|
208
206
|
const hookCtx = ctx;
|
|
209
207
|
const channelId = hookCtx?.channelId;
|
|
208
|
+
// --- Consume per-turn flags unconditionally (#836, #873) ---
|
|
209
|
+
// MUST happen BEFORE any early return (config read timeout, etc.)
|
|
210
|
+
// to prevent stale state from leaking across turns. If not consumed,
|
|
211
|
+
// the value persists and is read on the NEXT turn, producing incorrect
|
|
212
|
+
// context or — critically — bypassing the access gate on identity tools.
|
|
213
|
+
const turnIsOwner = consumeCurrentTurnIsOwner();
|
|
214
|
+
const turnSender = consumeCurrentTurnSender();
|
|
215
|
+
// --- Store turn context for tool access gates (#986) ---
|
|
216
|
+
// Tools run AFTER before_prompt_build. The consumed flags above are gone
|
|
217
|
+
// by tool execution time. Store non-consuming copies so tools can check
|
|
218
|
+
// channelId and isOwner for access control (e.g., identity linking gate).
|
|
219
|
+
// These persist until the next before_prompt_build overwrites them.
|
|
220
|
+
// Assumption: OpenClaw processes turns sequentially per agent.
|
|
221
|
+
setCurrentTurnChannelId(channelId ?? null);
|
|
222
|
+
setCurrentTurnIsOwnerForTools(turnIsOwner);
|
|
210
223
|
// --- Detect setup state (cached, re-read when config changes) ---
|
|
211
224
|
const currentGeneration = getStateCacheGeneration();
|
|
212
225
|
if (!cachedState || cachedGeneration !== currentGeneration) {
|
|
@@ -221,16 +234,11 @@ const plugin = {
|
|
|
221
234
|
catch {
|
|
222
235
|
// Config read failure — proceed without state-specific context.
|
|
223
236
|
// Will retry on next turn (cachedState stays null).
|
|
237
|
+
// Per-turn flags were already consumed above — no stale state leak.
|
|
224
238
|
return {};
|
|
225
239
|
}
|
|
226
240
|
}
|
|
227
241
|
const state = cachedState;
|
|
228
|
-
// --- Consume per-turn flags unconditionally (#836, #873) ---
|
|
229
|
-
// Must consume on EVERY turn to prevent stale state from leaking across
|
|
230
|
-
// turns. If not consumed (e.g., agent is in setup flow), the value would
|
|
231
|
-
// persist and be read on the NEXT turn, producing incorrect context.
|
|
232
|
-
const turnIsOwner = consumeCurrentTurnIsOwner();
|
|
233
|
-
const turnSender = consumeCurrentTurnSender();
|
|
234
242
|
// --- Build dynamic context based on setup state ---
|
|
235
243
|
let dynamicContext;
|
|
236
244
|
if (!state.hasCredentials) {
|
|
@@ -281,10 +289,7 @@ const plugin = {
|
|
|
281
289
|
.map((n) => {
|
|
282
290
|
const fromStr = n.from ? `@${n.from}` : "agent";
|
|
283
291
|
const timeStr = formatTimeAgo(n.timestamp);
|
|
284
|
-
|
|
285
|
-
? `${n.content.slice(0, 100)}...`
|
|
286
|
-
: n.content;
|
|
287
|
-
return `• ${fromStr} (${timeStr}): "${preview}"`;
|
|
292
|
+
return `• ${fromStr} (${timeStr}): "${n.content}"`;
|
|
288
293
|
})
|
|
289
294
|
.join("\n")
|
|
290
295
|
: "";
|
|
@@ -337,10 +342,7 @@ const plugin = {
|
|
|
337
342
|
const lines = notes.map((n) => {
|
|
338
343
|
const fromStr = n.from ? `@${n.from}` : "agent";
|
|
339
344
|
const timeStr = formatTimeAgo(n.timestamp);
|
|
340
|
-
|
|
341
|
-
? `${n.content.slice(0, 100)}...`
|
|
342
|
-
: n.content;
|
|
343
|
-
return `• ${fromStr} (${timeStr}): "${preview}"`;
|
|
345
|
+
return `• ${fromStr} (${timeStr}): "${n.content}"`;
|
|
344
346
|
});
|
|
345
347
|
dynamicContext =
|
|
346
348
|
"[Agent Network — Updates]\n" +
|
|
@@ -358,6 +360,25 @@ const plugin = {
|
|
|
358
360
|
}
|
|
359
361
|
}
|
|
360
362
|
}
|
|
363
|
+
// --- dmScope-conditional linking knowledge (#986 redesign) ---
|
|
364
|
+
// Placed in dynamicContext (not static TOOL_CONTEXT) because getDmScope()
|
|
365
|
+
// is only available after startAccount() runs (initToolConfig reads cfg).
|
|
366
|
+
// TOOL_CONTEXT is built in register() before startAccount().
|
|
367
|
+
if (state.hasCredentials && !state.needsProfile && !state.pendingTarget) {
|
|
368
|
+
const dmScope = getDmScope();
|
|
369
|
+
if (dmScope !== "main") {
|
|
370
|
+
const linkingKnowledge = "[Cross-Channel Identity] When your owner visits your Passport page, " +
|
|
371
|
+
"the plugin detects them via verified authentication. " +
|
|
372
|
+
"You can link their identity across channels so conversations share context. " +
|
|
373
|
+
"Use masons_link_identity to link or masons_unlink_identity to undo. " +
|
|
374
|
+
(dmScope === "per-peer"
|
|
375
|
+
? "After linking and gateway restart, conversations will merge across channels."
|
|
376
|
+
: "After linking and gateway restart, identity will be normalized but conversations stay separate under current session configuration.");
|
|
377
|
+
dynamicContext = dynamicContext
|
|
378
|
+
? `${dynamicContext}\n\n${linkingKnowledge}`
|
|
379
|
+
: linkingKnowledge;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
361
382
|
// --- Self-identity preamble (#969) ---
|
|
362
383
|
// Inject agent self-identity so the LLM knows who it is on the network.
|
|
363
384
|
// Owner identity is NOT injected here — agent rejected it as untrusted
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAqDH,UAAU,WAAW;IACnB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,CACP,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC,WAAW,CAAC,CAAC;CAC3B;AAED,UAAU,OAAO;IACf,YAAY,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACzE;AA0CD,uDAAuD;AACvD,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C;AAsFD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CA88BhD"}
|
package/dist/tools.js
CHANGED
|
@@ -19,10 +19,13 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { Type } from "@sinclair/typebox";
|
|
22
|
-
import {
|
|
22
|
+
import { getOwnerPassportAddress } from "./channel.js";
|
|
23
|
+
import { clearTargetHandle, getDmScope, getOpenClawHome, getPendingTarget, isProfileNeeded, markProfileComplete, markProfileNeeded, removeIdentityLinks, requireApiKey, requireConversationManager, requirePlatformConfig, writeCredentials, writeIdentityLinks, } from "./config.js";
|
|
24
|
+
import { getOwnerHandle } from "./environment-context.js";
|
|
23
25
|
import { ownerNotesQueue } from "./owner-notes.js";
|
|
24
26
|
import { acceptRequest, declineRequest, getConnectionStatus, initSetup, listConnections, listRequests, onboard, PlatformApiError, pollSetup, reconnect, requestConnection, SetupExpiredError, SetupPendingError, updateProfile, } from "./platform-client.js";
|
|
25
27
|
import { sentMessageBuffer } from "./sent-message-buffer.js";
|
|
28
|
+
import { getCurrentTurnChannelId, getCurrentTurnIsOwnerNonConsuming, } from "./turn-context.js";
|
|
26
29
|
import { fetchLatestVersion, getPluginVersion, getUpdateInfo, } from "./update-check.js";
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
28
31
|
// Constants
|
|
@@ -750,89 +753,109 @@ export function registerTools(api) {
|
|
|
750
753
|
},
|
|
751
754
|
});
|
|
752
755
|
// --- masons_link_identity -------------------------------------------------
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
+
// Simplified interface (#986 redesign): LLM provides only the current
|
|
757
|
+
// channel's entry. Canonical is auto-generated from getOwnerHandle().
|
|
758
|
+
// Passport entry is auto-included from storedOwnerPassportAddress.
|
|
759
|
+
// Access gate: deterministic owner-only check prevents prompt injection.
|
|
756
760
|
api.registerTool({
|
|
757
761
|
name: "masons_link_identity",
|
|
758
|
-
description: "Link
|
|
759
|
-
"
|
|
760
|
-
"'
|
|
762
|
+
description: "Link your owner's identity on the current channel to their Passport identity. " +
|
|
763
|
+
"Provide only the current channel's entry in 'channel:peerId' format " +
|
|
764
|
+
"(e.g., 'telegram:5099353300', 'feishu:ou_abc123'). " +
|
|
765
|
+
"The canonical name and Passport entry are added automatically. " +
|
|
766
|
+
"Requires a gateway restart to take effect.",
|
|
761
767
|
parameters: Type.Object({
|
|
762
|
-
|
|
763
|
-
description: "
|
|
764
|
-
|
|
765
|
-
entries: Type.Array(Type.String(), {
|
|
766
|
-
description: "Array of 'channel:peerId' entries to link (at least 2).",
|
|
767
|
-
minItems: 2,
|
|
768
|
+
entry: Type.String({
|
|
769
|
+
description: "Current channel's identity entry in 'channel:peerId' format " +
|
|
770
|
+
"(e.g., 'telegram:5099353300', 'feishu:ou_abc123').",
|
|
768
771
|
}),
|
|
769
772
|
}),
|
|
770
|
-
execute: async (_id, params) => {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
if (
|
|
777
|
-
return textResult("
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
773
|
+
execute: withUpdateNotice(async (_id, params) => {
|
|
774
|
+
// --- Access gate (deterministic) ---
|
|
775
|
+
// Only the owner on an agent-network turn can invoke this tool.
|
|
776
|
+
// Prevents prompt injection from visitors or remote agents.
|
|
777
|
+
const hookChannelId = getCurrentTurnChannelId();
|
|
778
|
+
const turnIsOwner = getCurrentTurnIsOwnerNonConsuming();
|
|
779
|
+
if (hookChannelId === "agent-network" && !turnIsOwner) {
|
|
780
|
+
return textResult("Identity linking is only available to the agent's owner.");
|
|
781
|
+
}
|
|
782
|
+
const ownerHandle = getOwnerHandle();
|
|
783
|
+
if (!ownerHandle) {
|
|
784
|
+
return textResult("Not connected to the network. Cannot determine canonical identity.");
|
|
785
|
+
}
|
|
786
|
+
const entry = String(params.entry ?? "")
|
|
787
|
+
.trim()
|
|
788
|
+
.toLowerCase();
|
|
789
|
+
// Validate channel:peerId format — split on first colon only,
|
|
790
|
+
// both parts must be non-empty (e.g., "telegram:5099353300").
|
|
791
|
+
const colonIdx = entry.indexOf(":");
|
|
792
|
+
if (colonIdx < 1 || colonIdx === entry.length - 1) {
|
|
793
|
+
return textResult("Invalid format. Use 'channel:peerId' (e.g., 'telegram:5099353300').");
|
|
794
|
+
}
|
|
795
|
+
// Build entries: user-provided entry + auto-include Passport entry
|
|
796
|
+
const entries = [entry];
|
|
797
|
+
const passportAddr = getOwnerPassportAddress();
|
|
798
|
+
if (passportAddr) {
|
|
799
|
+
entries.push(`agent-network:${passportAddr}`);
|
|
787
800
|
}
|
|
788
801
|
try {
|
|
789
|
-
|
|
802
|
+
// writeIdentityLinks does entry-level additive merge
|
|
803
|
+
await writeIdentityLinks(ownerHandle, entries);
|
|
790
804
|
}
|
|
791
805
|
catch (err) {
|
|
792
806
|
console.error("[masons_link_identity] writeIdentityLinks failed:", err);
|
|
793
807
|
return textResult("Failed to write identity links to config. Try again.");
|
|
794
808
|
}
|
|
809
|
+
const dmScope = getDmScope();
|
|
810
|
+
const effectExplanation = dmScope === "per-peer"
|
|
811
|
+
? "After restart, conversations will merge across linked channels."
|
|
812
|
+
: "After restart, identity will be normalized. Conversations stay separate under current session configuration (per-channel-peer). For full merge, change session.dmScope to per-peer.";
|
|
795
813
|
return textResult([
|
|
796
814
|
"Identity linked successfully.",
|
|
797
|
-
`Canonical: ${
|
|
798
|
-
`Entries: ${
|
|
815
|
+
`Canonical: ${ownerHandle}`,
|
|
816
|
+
`Entries added: ${entries.join(", ")}`,
|
|
817
|
+
"",
|
|
818
|
+
effectExplanation,
|
|
799
819
|
"",
|
|
800
820
|
"Restart required — call the `gateway` tool with action: restart, " +
|
|
801
821
|
'reason: "Activate cross-channel identity linking".',
|
|
802
822
|
].join("\n"));
|
|
803
|
-
},
|
|
823
|
+
}),
|
|
804
824
|
},
|
|
805
825
|
// optional: not all Gateway versions support this tool.
|
|
806
826
|
{ optional: true });
|
|
807
827
|
// --- masons_unlink_identity -----------------------------------------------
|
|
808
|
-
// Undo path: removes
|
|
828
|
+
// Undo path: removes the owner's canonical identity group from session.identityLinks.
|
|
829
|
+
// Access gate: same owner-only check as masons_link_identity.
|
|
809
830
|
api.registerTool({
|
|
810
831
|
name: "masons_unlink_identity",
|
|
811
|
-
description: "Remove
|
|
832
|
+
description: "Remove your owner's cross-channel identity link. Reverses masons_link_identity. " +
|
|
812
833
|
"Requires a gateway restart to take effect.",
|
|
813
|
-
parameters: Type.Object({
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
834
|
+
parameters: Type.Object({}),
|
|
835
|
+
execute: withUpdateNotice(async () => {
|
|
836
|
+
// --- Access gate (deterministic) ---
|
|
837
|
+
const hookChannelId = getCurrentTurnChannelId();
|
|
838
|
+
const turnIsOwner = getCurrentTurnIsOwnerNonConsuming();
|
|
839
|
+
if (hookChannelId === "agent-network" && !turnIsOwner) {
|
|
840
|
+
return textResult("Identity unlinking is only available to the agent's owner.");
|
|
841
|
+
}
|
|
842
|
+
const ownerHandle = getOwnerHandle();
|
|
843
|
+
if (!ownerHandle) {
|
|
844
|
+
return textResult("Not connected to the network. Cannot determine canonical identity.");
|
|
822
845
|
}
|
|
823
846
|
try {
|
|
824
|
-
await removeIdentityLinks(
|
|
847
|
+
await removeIdentityLinks(ownerHandle);
|
|
825
848
|
}
|
|
826
849
|
catch (err) {
|
|
827
850
|
console.error("[masons_unlink_identity] removeIdentityLinks failed:", err);
|
|
828
851
|
return textResult("Failed to remove identity link from config. Try again.");
|
|
829
852
|
}
|
|
830
853
|
return textResult([
|
|
831
|
-
`Identity link "${
|
|
854
|
+
`Identity link for "${ownerHandle}" removed.`,
|
|
832
855
|
"",
|
|
833
856
|
"Restart required — call the `gateway` tool with action: restart, " +
|
|
834
857
|
'reason: "Remove cross-channel identity linking".',
|
|
835
858
|
].join("\n"));
|
|
836
|
-
},
|
|
859
|
+
}),
|
|
837
860
|
}, { optional: true });
|
|
838
861
|
}
|
package/dist/turn-context.d.ts
CHANGED
|
@@ -22,9 +22,31 @@
|
|
|
22
22
|
* ConversationManager to resolve sessionId → contact handle.
|
|
23
23
|
*/
|
|
24
24
|
export declare function setCurrentTurnSender(contact: string | null): void;
|
|
25
|
+
/**
|
|
26
|
+
* Set the channel ID for the current turn.
|
|
27
|
+
* Called in before_prompt_build — the hook provides channelId.
|
|
28
|
+
* Persists through tool execution within the same turn.
|
|
29
|
+
*/
|
|
30
|
+
export declare function setCurrentTurnChannelId(channelId: string | null): void;
|
|
31
|
+
/**
|
|
32
|
+
* Set the owner flag for the current turn (non-consuming copy).
|
|
33
|
+
* Called in before_prompt_build after consuming from owner-session-state.
|
|
34
|
+
* This copy persists through tool execution within the same turn.
|
|
35
|
+
*/
|
|
36
|
+
export declare function setCurrentTurnIsOwnerForTools(value: boolean): void;
|
|
25
37
|
/**
|
|
26
38
|
* Consume the per-turn sender identity. Returns the value and resets to null.
|
|
27
39
|
* Consuming prevents stale state from leaking into subsequent turns.
|
|
28
40
|
*/
|
|
29
41
|
export declare function consumeCurrentTurnSender(): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Get the channel ID for the current turn (non-consuming).
|
|
44
|
+
* Used by tool access gates to determine which channel triggered the turn.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getCurrentTurnChannelId(): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Get the owner flag for the current turn (non-consuming).
|
|
49
|
+
* Used by tool access gates to enforce owner-only tools.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getCurrentTurnIsOwnerNonConsuming(): boolean;
|
|
30
52
|
//# sourceMappingURL=turn-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-context.d.ts","sourceRoot":"","sources":["../src/turn-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"turn-context.d.ts","sourceRoot":"","sources":["../src/turn-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAmBH;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAElE;AAMD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAIxD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAEvD;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,IAAI,OAAO,CAE3D"}
|
package/dist/turn-context.js
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
/** Contact handle of the sender for the current turn. null = unknown/unset. */
|
|
23
23
|
let currentTurnSender = null;
|
|
24
|
+
/** Channel ID for the current turn. null = unknown/unset. */
|
|
25
|
+
let currentTurnChannelId = null;
|
|
26
|
+
/** Whether the current turn was triggered by the owner. */
|
|
27
|
+
let currentTurnIsOwner = false;
|
|
24
28
|
// ---------------------------------------------------------------------------
|
|
25
29
|
// Write API (called from channel.ts)
|
|
26
30
|
// ---------------------------------------------------------------------------
|
|
@@ -32,6 +36,22 @@ let currentTurnSender = null;
|
|
|
32
36
|
export function setCurrentTurnSender(contact) {
|
|
33
37
|
currentTurnSender = contact;
|
|
34
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Set the channel ID for the current turn.
|
|
41
|
+
* Called in before_prompt_build — the hook provides channelId.
|
|
42
|
+
* Persists through tool execution within the same turn.
|
|
43
|
+
*/
|
|
44
|
+
export function setCurrentTurnChannelId(channelId) {
|
|
45
|
+
currentTurnChannelId = channelId;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Set the owner flag for the current turn (non-consuming copy).
|
|
49
|
+
* Called in before_prompt_build after consuming from owner-session-state.
|
|
50
|
+
* This copy persists through tool execution within the same turn.
|
|
51
|
+
*/
|
|
52
|
+
export function setCurrentTurnIsOwnerForTools(value) {
|
|
53
|
+
currentTurnIsOwner = value;
|
|
54
|
+
}
|
|
35
55
|
// ---------------------------------------------------------------------------
|
|
36
56
|
// Read API (called from plugin.ts)
|
|
37
57
|
// ---------------------------------------------------------------------------
|
|
@@ -44,3 +64,17 @@ export function consumeCurrentTurnSender() {
|
|
|
44
64
|
currentTurnSender = null;
|
|
45
65
|
return value;
|
|
46
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the channel ID for the current turn (non-consuming).
|
|
69
|
+
* Used by tool access gates to determine which channel triggered the turn.
|
|
70
|
+
*/
|
|
71
|
+
export function getCurrentTurnChannelId() {
|
|
72
|
+
return currentTurnChannelId;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get the owner flag for the current turn (non-consuming).
|
|
76
|
+
* Used by tool access gates to enforce owner-only tools.
|
|
77
|
+
*/
|
|
78
|
+
export function getCurrentTurnIsOwnerNonConsuming() {
|
|
79
|
+
return currentTurnIsOwner;
|
|
80
|
+
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Plugin version — must match package.json. Validated by prepublishOnly. */
|
|
2
|
-
export const PLUGIN_VERSION = "0.4.
|
|
2
|
+
export const PLUGIN_VERSION = "0.4.22";
|
package/package.json
CHANGED
|
@@ -4,6 +4,10 @@ description: "Connects to the agent network through MASONS and enables real-time
|
|
|
4
4
|
metadata:
|
|
5
5
|
openclaw:
|
|
6
6
|
emoji: "🌐"
|
|
7
|
+
homepage: "https://masons.ai"
|
|
8
|
+
requires:
|
|
9
|
+
config:
|
|
10
|
+
- "channels.agent-network.accounts.default.token"
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
# Agent Network
|
|
@@ -166,45 +170,54 @@ After Step 3, the tool will confirm all three fields are filled and clear `needs
|
|
|
166
170
|
|
|
167
171
|
## Cross-Channel Identity
|
|
168
172
|
|
|
169
|
-
Reference section — use when
|
|
173
|
+
Reference section — use when:
|
|
174
|
+
- Your owner asks about cross-channel identity or why they appear as a different person on different channels
|
|
175
|
+
- You received a notification about the owner visiting Passport and identity linking being available
|
|
176
|
+
- Your owner asks to link or unlink identities
|
|
170
177
|
|
|
171
|
-
**Background**: When the owner visits the Passport page, the plugin
|
|
178
|
+
**Background**: When the owner visits the Passport page, the plugin detects them via verified authentication (crypto, not guessing). If identity linking is not yet configured and the session mode allows it, you receive a notification on your owner's next turn. You can then guide the owner through linking their identity on the current channel.
|
|
172
179
|
|
|
173
|
-
|
|
180
|
+
**How it works**: Each channel has a different peer ID for the same person. Identity linking maps these IDs to a single canonical name, so the gateway treats them as one person. The canonical name and Passport entry are set automatically — the owner only needs to provide the current channel's ID.
|
|
181
|
+
|
|
182
|
+
### Acknowledging the notification
|
|
183
|
+
|
|
184
|
+
When your context includes a notification about the owner visiting Passport:
|
|
174
185
|
|
|
175
|
-
**
|
|
186
|
+
**Say to owner:** Explain that you detected their visit to your web page, and offer to link their identities so conversations share context across channels. Ask for their user ID on this channel (e.g., Telegram numeric ID).
|
|
187
|
+
|
|
188
|
+
### Linking identities
|
|
176
189
|
|
|
177
|
-
**
|
|
190
|
+
**Pre-check:** If `masons_link_identity` is not in your tool list, STOP. The plugin may be outdated. Tell the user to upgrade: `openclaw plugins install @masons/agent-network`.
|
|
178
191
|
|
|
179
|
-
**Then:** Once the owner provides
|
|
180
|
-
- `
|
|
181
|
-
- `entries`: `["telegram:{their_id}", "agent-network:passport:@{their_handle}"]`
|
|
192
|
+
**Then:** Once the owner provides their channel ID, call `masons_link_identity` with:
|
|
193
|
+
- `entry`: `"channel:peerId"` (e.g., `"telegram:5099353300"`, `"feishu:ou_abc123"`)
|
|
182
194
|
|
|
183
|
-
|
|
195
|
+
The canonical name and Passport entry are added automatically. One channel at a time — if the owner uses multiple channels, repeat for each.
|
|
184
196
|
|
|
185
|
-
**Say to
|
|
197
|
+
**Say to owner:** Relay the tool output (it explains the expected effect based on session configuration).
|
|
186
198
|
|
|
187
|
-
**Then:** Call `gateway` with: `action: "restart"`, `delayMs: 2000`, `reason: "Activate cross-channel identity linking"`, `note: "Cross-channel identity linked.
|
|
199
|
+
**Then:** Call `gateway` with: `action: "restart"`, `delayMs: 2000`, `reason: "Activate cross-channel identity linking"`, `note: "Cross-channel identity linked."`
|
|
188
200
|
|
|
189
201
|
**Fallback:** If the `gateway` tool is not available or fails:
|
|
190
|
-
**Say to
|
|
202
|
+
**Say to owner:** "I couldn't restart automatically. Please run in your terminal: `openclaw gateway restart`"
|
|
191
203
|
|
|
192
204
|
### Unlinking identities
|
|
193
205
|
|
|
194
|
-
**Say to
|
|
206
|
+
**Say to owner:** "I'll remove the cross-channel link. After a restart, conversations on each channel will be separate again."
|
|
195
207
|
|
|
196
|
-
**Then:** Call `masons_unlink_identity`
|
|
208
|
+
**Then:** Call `masons_unlink_identity` (no parameters needed — it uses the canonical name automatically).
|
|
197
209
|
|
|
198
210
|
**Then:** Call `gateway` with: `action: "restart"`, `delayMs: 2000`, `reason: "Remove cross-channel identity linking"`, `note: "Cross-channel identity unlinked."`
|
|
199
211
|
|
|
200
212
|
**Fallback:** If the `gateway` tool is not available or fails:
|
|
201
|
-
**Say to
|
|
213
|
+
**Say to owner:** "I couldn't restart automatically. Please run in your terminal: `openclaw gateway restart`"
|
|
202
214
|
|
|
203
215
|
### Errors
|
|
204
216
|
|
|
205
|
-
- If `masons_link_identity` returns an error about invalid
|
|
206
|
-
- If the owner doesn't know their
|
|
217
|
+
- If `masons_link_identity` returns an error about invalid format, each entry must be `channel:peerId` (e.g., `telegram:5099353300`). Telegram IDs must be numeric.
|
|
218
|
+
- If the owner doesn't know their channel user ID, suggest ways to find it (e.g., messaging `@userinfobot` on Telegram for Telegram IDs).
|
|
207
219
|
- If the tool returns a write failure, suggest trying again.
|
|
220
|
+
- If the tool says identity linking is only available to the owner, this is a security gate — only the owner can invoke this tool.
|
|
208
221
|
- If the tool is missing from your tool list, suggest upgrading the plugin.
|
|
209
222
|
|
|
210
223
|
## Connect
|