@clawling/clawchat-plugin-openclaw 2026.9.10-1 → 2026.9.11-1
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/src/llm-context-debug.js +7 -1
- package/dist/src/permission-result.js +42 -12
- package/dist/src/runtime.js +24 -4
- package/dist/src/terminal-send.js +16 -1
- package/package.json +1 -1
- package/src/llm-context-debug.ts +7 -1
- package/src/permission-result.ts +46 -11
- package/src/runtime.ts +31 -3
- package/src/terminal-send.ts +16 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { resolveStateDirFromEnv } from "./state-paths.js";
|
|
3
4
|
function envEnabled(value) {
|
|
4
5
|
return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
|
|
5
6
|
}
|
|
@@ -10,7 +11,12 @@ function safeId(value) {
|
|
|
10
11
|
export function createOpenclawLlmContextDebug(params = {}) {
|
|
11
12
|
const env = params.env ?? process.env;
|
|
12
13
|
const enabled = envEnabled(env.CLAWCHAT_LLM_CONTEXT_DEBUG);
|
|
13
|
-
|
|
14
|
+
// Default under the per-instance OpenClaw state dir, never the process cwd:
|
|
15
|
+
// two profiles launched from one directory must not interleave runs and
|
|
16
|
+
// overwrite each other's `latest.json`.
|
|
17
|
+
const snapshotRoot = path.resolve(env.CLAWCHAT_LLM_CONTEXT_SNAPSHOT_DIR ||
|
|
18
|
+
params.defaultSnapshotRoot ||
|
|
19
|
+
path.join(resolveStateDirFromEnv(env), "clawchat", "llm-context-debug"));
|
|
14
20
|
return {
|
|
15
21
|
enabled,
|
|
16
22
|
captureFullInput: envEnabled(env.CLAWCHAT_LLM_CONTEXT_CAPTURE_FULL_INPUT),
|
|
@@ -20,16 +20,42 @@
|
|
|
20
20
|
* The discriminator is `payload.metadata.kind === "permission_result"`.
|
|
21
21
|
*/
|
|
22
22
|
import { EVENT } from "./protocol-types.js";
|
|
23
|
-
// Module-level dedup set — `request_id` values are stable backend identifiers.
|
|
24
|
-
// A duplicate receipt (e.g. from WS replay on reconnect) must not re-trigger
|
|
25
|
-
// an agent turn.
|
|
26
|
-
const _seenRequestIds = new Set();
|
|
27
23
|
/**
|
|
28
|
-
*
|
|
24
|
+
* Upper bound on retained `request_id`s per account (FIFO eviction). Mirrors
|
|
25
|
+
* `createNotifySignalObserver` in `src/ws-alignment.ts`: permission receipts
|
|
26
|
+
* are rare (one per owner decision), so 512 covers any realistic replay window
|
|
27
|
+
* while keeping a long-running gateway's memory flat.
|
|
28
|
+
*/
|
|
29
|
+
export const PERMISSION_RESULT_DEDUP_MAX_SEEN = 512;
|
|
30
|
+
// Dedup state partitioned by accountId — `request_id` values are stable backend
|
|
31
|
+
// identifiers scoped to the agent identity that raised the request, so one
|
|
32
|
+
// account's history must never suppress another account's receipt with the
|
|
33
|
+
// same id. A duplicate receipt within one account (e.g. from WS replay on
|
|
34
|
+
// reconnect) must not re-trigger an agent turn.
|
|
35
|
+
const _seenRequestIdsByAccount = new Map();
|
|
36
|
+
function seenRequestIdsFor(accountId) {
|
|
37
|
+
let entry = _seenRequestIdsByAccount.get(accountId);
|
|
38
|
+
if (!entry) {
|
|
39
|
+
entry = { seen: new Set(), order: [] };
|
|
40
|
+
_seenRequestIdsByAccount.set(accountId, entry);
|
|
41
|
+
}
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
function markRequestIdSeen(entry, requestId) {
|
|
45
|
+
entry.seen.add(requestId);
|
|
46
|
+
entry.order.push(requestId);
|
|
47
|
+
while (entry.order.length > PERMISSION_RESULT_DEDUP_MAX_SEEN) {
|
|
48
|
+
const evicted = entry.order.shift();
|
|
49
|
+
if (evicted !== undefined)
|
|
50
|
+
entry.seen.delete(evicted);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @internal Test helper — reset the dedup state between isolated test runs.
|
|
29
55
|
* Never call this in production code.
|
|
30
56
|
*/
|
|
31
57
|
export function _resetPermissionResultDedup() {
|
|
32
|
-
|
|
58
|
+
_seenRequestIdsByAccount.clear();
|
|
33
59
|
}
|
|
34
60
|
function buildOutcomeNote(metadata) {
|
|
35
61
|
const { operation, outcome, reason } = metadata;
|
|
@@ -107,8 +133,10 @@ export function buildPermissionResultEnvelope(params) {
|
|
|
107
133
|
};
|
|
108
134
|
}
|
|
109
135
|
/**
|
|
110
|
-
* Handles a `permission_result` receipt. Deduplicates by `request_id`
|
|
111
|
-
* (idempotent — WS replay on reconnect may deliver the
|
|
136
|
+
* Handles a `permission_result` receipt. Deduplicates by `request_id` within
|
|
137
|
+
* the receiving account (idempotent — WS replay on reconnect may deliver the
|
|
138
|
+
* same receipt again), retaining at most {@link PERMISSION_RESULT_DEDUP_MAX_SEEN}
|
|
139
|
+
* ids per account (oldest evicted first).
|
|
112
140
|
*
|
|
113
141
|
* An empty/falsy `request_id` is never added to the dedup set — two receipts
|
|
114
142
|
* can legitimately share an empty id, and both must still be rendered — but
|
|
@@ -127,9 +155,11 @@ export function buildPermissionResultEnvelope(params) {
|
|
|
127
155
|
export function handlePermissionResult(metadata, account, ownerConversationId) {
|
|
128
156
|
if (!ownerConversationId)
|
|
129
157
|
return null;
|
|
130
|
-
if (metadata.request_id
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
if (metadata.request_id) {
|
|
159
|
+
const entry = seenRequestIdsFor(account.accountId);
|
|
160
|
+
if (entry.seen.has(metadata.request_id))
|
|
161
|
+
return null;
|
|
162
|
+
markRequestIdSeen(entry, metadata.request_id);
|
|
163
|
+
}
|
|
134
164
|
return buildPermissionResultEnvelope({ account, metadata, ownerConversationId });
|
|
135
165
|
}
|
package/dist/src/runtime.js
CHANGED
|
@@ -12,7 +12,7 @@ import { LivewareSampleSupervisor, } from "./liveware-sample.js";
|
|
|
12
12
|
import { ClawlingApiError } from "./api-types.js";
|
|
13
13
|
import { RefreshManager } from "./refresh-manager.js";
|
|
14
14
|
import { runOpenclawClawlingLogin, } from "./login.runtime.js";
|
|
15
|
-
import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
15
|
+
import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, resolveOpenclawClawlingAccount, CLAWCHAT_REFRESH_TOKEN_ENV, } from "./config.js";
|
|
16
16
|
import { isValidChatId } from "./ws-client.js";
|
|
17
17
|
import { dispatchOpenclawClawlingInbound } from "./inbound.js";
|
|
18
18
|
import { PendingConsentStore, cleanupTombstonedManagedSkills, handleOwnerConsentReply, managedSkillExists, readLocalSkillState, resolveBundledSkillsDir, resolveManagedSkillsDir, runSkillUpdateCheck, } from "./skill-update.js";
|
|
@@ -131,11 +131,25 @@ export function classifyHelloFailReason(reason) {
|
|
|
131
131
|
return "generic";
|
|
132
132
|
}
|
|
133
133
|
/** Read `channels.<CHANNEL_ID>.refreshToken` from a live config, or null. */
|
|
134
|
-
|
|
134
|
+
/**
|
|
135
|
+
* The refresh token paired with the configured access token. Channel config
|
|
136
|
+
* `refreshToken` wins. `CLAWCHAT_REFRESH_TOKEN` (declared in the manifest's
|
|
137
|
+
* `channelEnvVars`) is honoured ONLY when the channel config carries no `token`
|
|
138
|
+
* of its own — i.e. when the access token itself came from `CLAWCHAT_TOKEN` —
|
|
139
|
+
* so an env-provisioned pair can rotate, while a login-written config token is
|
|
140
|
+
* never paired with a stray refresh token left in the environment.
|
|
141
|
+
*/
|
|
142
|
+
function readConfigRefreshToken(cfg, env = process.env) {
|
|
135
143
|
const channels = cfg.channels;
|
|
136
144
|
const channel = isRecord(channels) ? channels[CHANNEL_ID] : undefined;
|
|
137
145
|
const refreshToken = isRecord(channel) ? channel.refreshToken : undefined;
|
|
138
|
-
|
|
146
|
+
if (typeof refreshToken === "string" && refreshToken.trim())
|
|
147
|
+
return refreshToken.trim();
|
|
148
|
+
const configToken = isRecord(channel) ? channel.token : undefined;
|
|
149
|
+
if (typeof configToken === "string" && configToken.trim())
|
|
150
|
+
return null;
|
|
151
|
+
const envRefreshToken = env[CLAWCHAT_REFRESH_TOKEN_ENV];
|
|
152
|
+
return typeof envRefreshToken === "string" && envRefreshToken.trim() ? envRefreshToken.trim() : null;
|
|
139
153
|
}
|
|
140
154
|
function withFullVerboseDispatchConfig(cfg, agentId) {
|
|
141
155
|
const cfgRecord = cfg;
|
|
@@ -445,12 +459,17 @@ export async function maybeAutoActivateFromEnv(params) {
|
|
|
445
459
|
return params.account;
|
|
446
460
|
if (accountHasConnectCredentials(params.account))
|
|
447
461
|
return params.account;
|
|
462
|
+
const accountId = params.account.accountId;
|
|
463
|
+
const stored = params.store?.getActivationCredentials?.({ platform: "openclaw", accountId });
|
|
464
|
+
if (stored) {
|
|
465
|
+
params.log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} present but a sqlite activation row already holds credentials; not redeeming`);
|
|
466
|
+
return params.account;
|
|
467
|
+
}
|
|
448
468
|
if (consumedAutoActivationCodes.has(code))
|
|
449
469
|
return params.account;
|
|
450
470
|
consumedAutoActivationCodes.add(code);
|
|
451
471
|
const runLogin = params.runLogin ?? runOpenclawClawlingLogin;
|
|
452
472
|
const resolveAccount = params.resolveAccount ?? resolveOpenclawClawlingAccount;
|
|
453
|
-
const accountId = params.account.accountId;
|
|
454
473
|
params.log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} present; attempting non-interactive activation`);
|
|
455
474
|
try {
|
|
456
475
|
await runLogin({
|
|
@@ -722,6 +741,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
722
741
|
account,
|
|
723
742
|
accountId,
|
|
724
743
|
connectCode: envConnectCode,
|
|
744
|
+
store,
|
|
725
745
|
log,
|
|
726
746
|
});
|
|
727
747
|
}
|
|
@@ -8,13 +8,28 @@ function key(accountId, chatId, scopeId) {
|
|
|
8
8
|
export function runWithTerminalClawChatSendScope(scopeId, fn) {
|
|
9
9
|
return terminalSendScope.run(scopeId, fn);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Drop every marker whose TTL has lapsed. A marker is only otherwise deleted
|
|
13
|
+
* when the same dispatch scope consumes it, and a scope that never produces a
|
|
14
|
+
* follow-up reply for that chat (mention sent to another chat, `<clawchat:no-reply/>`,
|
|
15
|
+
* dispatch failure) leaves it behind; without this sweep the map would grow by
|
|
16
|
+
* one entry per such turn for the lifetime of the gateway.
|
|
17
|
+
*/
|
|
18
|
+
function sweepExpiredTerminalSends(nowMs) {
|
|
19
|
+
for (const [recordKey, record] of terminalSends) {
|
|
20
|
+
if (record.expiresAt <= nowMs)
|
|
21
|
+
terminalSends.delete(recordKey);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
11
24
|
export function markTerminalClawChatSend(params) {
|
|
12
25
|
const scopeId = params.scopeId ?? terminalSendScope.getStore();
|
|
13
26
|
if (!scopeId)
|
|
14
27
|
return;
|
|
28
|
+
const nowMs = params.nowMs ?? Date.now();
|
|
29
|
+
sweepExpiredTerminalSends(nowMs);
|
|
15
30
|
terminalSends.set(key(params.accountId, params.chatId, scopeId), {
|
|
16
31
|
messageId: params.messageId,
|
|
17
|
-
expiresAt:
|
|
32
|
+
expiresAt: nowMs + (params.ttlMs ?? DEFAULT_TTL_MS),
|
|
18
33
|
scopeId,
|
|
19
34
|
});
|
|
20
35
|
}
|
package/package.json
CHANGED
package/src/llm-context-debug.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { resolveStateDirFromEnv } from "./state-paths.ts";
|
|
3
4
|
|
|
4
5
|
export type OpenclawLlmContextVisibility =
|
|
5
6
|
| "full_llm_input"
|
|
@@ -60,8 +61,13 @@ export function createOpenclawLlmContextDebug(params: {
|
|
|
60
61
|
} = {}): OpenclawLlmContextDebug {
|
|
61
62
|
const env = params.env ?? process.env;
|
|
62
63
|
const enabled = envEnabled(env.CLAWCHAT_LLM_CONTEXT_DEBUG);
|
|
64
|
+
// Default under the per-instance OpenClaw state dir, never the process cwd:
|
|
65
|
+
// two profiles launched from one directory must not interleave runs and
|
|
66
|
+
// overwrite each other's `latest.json`.
|
|
63
67
|
const snapshotRoot = path.resolve(
|
|
64
|
-
env.CLAWCHAT_LLM_CONTEXT_SNAPSHOT_DIR ||
|
|
68
|
+
env.CLAWCHAT_LLM_CONTEXT_SNAPSHOT_DIR ||
|
|
69
|
+
params.defaultSnapshotRoot ||
|
|
70
|
+
path.join(resolveStateDirFromEnv(env as NodeJS.ProcessEnv), "clawchat", "llm-context-debug"),
|
|
65
71
|
);
|
|
66
72
|
|
|
67
73
|
return {
|
package/src/permission-result.ts
CHANGED
|
@@ -30,17 +30,47 @@ export interface PermissionResultMetadata {
|
|
|
30
30
|
request_id: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Upper bound on retained `request_id`s per account (FIFO eviction). Mirrors
|
|
35
|
+
* `createNotifySignalObserver` in `src/ws-alignment.ts`: permission receipts
|
|
36
|
+
* are rare (one per owner decision), so 512 covers any realistic replay window
|
|
37
|
+
* while keeping a long-running gateway's memory flat.
|
|
38
|
+
*/
|
|
39
|
+
export const PERMISSION_RESULT_DEDUP_MAX_SEEN = 512;
|
|
40
|
+
|
|
41
|
+
type SeenRequestIds = { seen: Set<string>; order: string[] };
|
|
42
|
+
|
|
43
|
+
// Dedup state partitioned by accountId — `request_id` values are stable backend
|
|
44
|
+
// identifiers scoped to the agent identity that raised the request, so one
|
|
45
|
+
// account's history must never suppress another account's receipt with the
|
|
46
|
+
// same id. A duplicate receipt within one account (e.g. from WS replay on
|
|
47
|
+
// reconnect) must not re-trigger an agent turn.
|
|
48
|
+
const _seenRequestIdsByAccount = new Map<string, SeenRequestIds>();
|
|
49
|
+
|
|
50
|
+
function seenRequestIdsFor(accountId: string): SeenRequestIds {
|
|
51
|
+
let entry = _seenRequestIdsByAccount.get(accountId);
|
|
52
|
+
if (!entry) {
|
|
53
|
+
entry = { seen: new Set<string>(), order: [] };
|
|
54
|
+
_seenRequestIdsByAccount.set(accountId, entry);
|
|
55
|
+
}
|
|
56
|
+
return entry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function markRequestIdSeen(entry: SeenRequestIds, requestId: string): void {
|
|
60
|
+
entry.seen.add(requestId);
|
|
61
|
+
entry.order.push(requestId);
|
|
62
|
+
while (entry.order.length > PERMISSION_RESULT_DEDUP_MAX_SEEN) {
|
|
63
|
+
const evicted = entry.order.shift();
|
|
64
|
+
if (evicted !== undefined) entry.seen.delete(evicted);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
37
67
|
|
|
38
68
|
/**
|
|
39
|
-
* @internal Test helper — reset the dedup
|
|
69
|
+
* @internal Test helper — reset the dedup state between isolated test runs.
|
|
40
70
|
* Never call this in production code.
|
|
41
71
|
*/
|
|
42
72
|
export function _resetPermissionResultDedup(): void {
|
|
43
|
-
|
|
73
|
+
_seenRequestIdsByAccount.clear();
|
|
44
74
|
}
|
|
45
75
|
|
|
46
76
|
function buildOutcomeNote(metadata: PermissionResultMetadata): string {
|
|
@@ -127,8 +157,10 @@ export function buildPermissionResultEnvelope(params: {
|
|
|
127
157
|
}
|
|
128
158
|
|
|
129
159
|
/**
|
|
130
|
-
* Handles a `permission_result` receipt. Deduplicates by `request_id`
|
|
131
|
-
* (idempotent — WS replay on reconnect may deliver the
|
|
160
|
+
* Handles a `permission_result` receipt. Deduplicates by `request_id` within
|
|
161
|
+
* the receiving account (idempotent — WS replay on reconnect may deliver the
|
|
162
|
+
* same receipt again), retaining at most {@link PERMISSION_RESULT_DEDUP_MAX_SEEN}
|
|
163
|
+
* ids per account (oldest evicted first).
|
|
132
164
|
*
|
|
133
165
|
* An empty/falsy `request_id` is never added to the dedup set — two receipts
|
|
134
166
|
* can legitimately share an empty id, and both must still be rendered — but
|
|
@@ -146,11 +178,14 @@ export function buildPermissionResultEnvelope(params: {
|
|
|
146
178
|
*/
|
|
147
179
|
export function handlePermissionResult(
|
|
148
180
|
metadata: PermissionResultMetadata,
|
|
149
|
-
account: Pick<ResolvedOpenclawClawlingAccount, "userId" | "ownerUserId">,
|
|
181
|
+
account: Pick<ResolvedOpenclawClawlingAccount, "accountId" | "userId" | "ownerUserId">,
|
|
150
182
|
ownerConversationId: string | null,
|
|
151
183
|
): Envelope | null {
|
|
152
184
|
if (!ownerConversationId) return null;
|
|
153
|
-
if (metadata.request_id
|
|
154
|
-
|
|
185
|
+
if (metadata.request_id) {
|
|
186
|
+
const entry = seenRequestIdsFor(account.accountId);
|
|
187
|
+
if (entry.seen.has(metadata.request_id)) return null;
|
|
188
|
+
markRequestIdSeen(entry, metadata.request_id);
|
|
189
|
+
}
|
|
155
190
|
return buildPermissionResultEnvelope({ account, metadata, ownerConversationId });
|
|
156
191
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
hasOpenclawClawlingConnectCredentials,
|
|
41
41
|
resolveOpenclawClawlingAccount,
|
|
42
42
|
type ResolvedOpenclawClawlingAccount,
|
|
43
|
+
CLAWCHAT_REFRESH_TOKEN_ENV,
|
|
43
44
|
} from "./config.ts";
|
|
44
45
|
import { isValidChatId, type ClawlingChatClient } from "./ws-client.ts";
|
|
45
46
|
import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inbound.ts";
|
|
@@ -234,11 +235,23 @@ export function classifyHelloFailReason(
|
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
/** Read `channels.<CHANNEL_ID>.refreshToken` from a live config, or null. */
|
|
237
|
-
|
|
238
|
+
/**
|
|
239
|
+
* The refresh token paired with the configured access token. Channel config
|
|
240
|
+
* `refreshToken` wins. `CLAWCHAT_REFRESH_TOKEN` (declared in the manifest's
|
|
241
|
+
* `channelEnvVars`) is honoured ONLY when the channel config carries no `token`
|
|
242
|
+
* of its own — i.e. when the access token itself came from `CLAWCHAT_TOKEN` —
|
|
243
|
+
* so an env-provisioned pair can rotate, while a login-written config token is
|
|
244
|
+
* never paired with a stray refresh token left in the environment.
|
|
245
|
+
*/
|
|
246
|
+
function readConfigRefreshToken(cfg: OpenClawConfig, env: NodeJS.ProcessEnv = process.env): string | null {
|
|
238
247
|
const channels = (cfg as { channels?: Record<string, unknown> }).channels;
|
|
239
248
|
const channel = isRecord(channels) ? channels[CHANNEL_ID] : undefined;
|
|
240
249
|
const refreshToken = isRecord(channel) ? channel.refreshToken : undefined;
|
|
241
|
-
|
|
250
|
+
if (typeof refreshToken === "string" && refreshToken.trim()) return refreshToken.trim();
|
|
251
|
+
const configToken = isRecord(channel) ? channel.token : undefined;
|
|
252
|
+
if (typeof configToken === "string" && configToken.trim()) return null;
|
|
253
|
+
const envRefreshToken = env[CLAWCHAT_REFRESH_TOKEN_ENV];
|
|
254
|
+
return typeof envRefreshToken === "string" && envRefreshToken.trim() ? envRefreshToken.trim() : null;
|
|
242
255
|
}
|
|
243
256
|
|
|
244
257
|
function withFullVerboseDispatchConfig(cfg: OpenClawConfig, agentId: string): OpenClawConfig {
|
|
@@ -673,6 +686,13 @@ export interface AutoActivateFromEnvParams {
|
|
|
673
686
|
accountId: string | null;
|
|
674
687
|
/** The connect code (production: `process.env[CONNECT_CODE_ENV]`). */
|
|
675
688
|
connectCode: string | undefined;
|
|
689
|
+
/**
|
|
690
|
+
* Plugin SQLite store. A complete activation row is the durable credential
|
|
691
|
+
* source of truth (`waitForActivationCredentials` resolves SQLite-first), so
|
|
692
|
+
* when one exists the env code is skipped — NOT consumed — rather than
|
|
693
|
+
* re-redeemed on every restart of a container that still carries the code.
|
|
694
|
+
*/
|
|
695
|
+
store?: Pick<RuntimeConnectionStore, "getActivationCredentials"> | null;
|
|
676
696
|
log?: Log;
|
|
677
697
|
/** Test seam — login implementation. */
|
|
678
698
|
runLogin?: (params: LoginParams) => Promise<void>;
|
|
@@ -695,12 +715,19 @@ export async function maybeAutoActivateFromEnv(
|
|
|
695
715
|
const code = (params.connectCode ?? "").trim();
|
|
696
716
|
if (!code) return params.account;
|
|
697
717
|
if (accountHasConnectCredentials(params.account)) return params.account;
|
|
718
|
+
const accountId = params.account.accountId;
|
|
719
|
+
const stored = params.store?.getActivationCredentials?.({ platform: "openclaw", accountId });
|
|
720
|
+
if (stored) {
|
|
721
|
+
params.log?.info?.(
|
|
722
|
+
`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} present but a sqlite activation row already holds credentials; not redeeming`,
|
|
723
|
+
);
|
|
724
|
+
return params.account;
|
|
725
|
+
}
|
|
698
726
|
if (consumedAutoActivationCodes.has(code)) return params.account;
|
|
699
727
|
consumedAutoActivationCodes.add(code);
|
|
700
728
|
|
|
701
729
|
const runLogin = params.runLogin ?? runOpenclawClawlingLogin;
|
|
702
730
|
const resolveAccount = params.resolveAccount ?? resolveOpenclawClawlingAccount;
|
|
703
|
-
const accountId = params.account.accountId;
|
|
704
731
|
params.log?.info?.(
|
|
705
732
|
`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} present; attempting non-interactive activation`,
|
|
706
733
|
);
|
|
@@ -1044,6 +1071,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1044
1071
|
account,
|
|
1045
1072
|
accountId,
|
|
1046
1073
|
connectCode: envConnectCode,
|
|
1074
|
+
store,
|
|
1047
1075
|
log,
|
|
1048
1076
|
});
|
|
1049
1077
|
}
|
package/src/terminal-send.ts
CHANGED
|
@@ -18,6 +18,19 @@ export function runWithTerminalClawChatSendScope<T>(scopeId: string, fn: () => T
|
|
|
18
18
|
return terminalSendScope.run(scopeId, fn);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Drop every marker whose TTL has lapsed. A marker is only otherwise deleted
|
|
23
|
+
* when the same dispatch scope consumes it, and a scope that never produces a
|
|
24
|
+
* follow-up reply for that chat (mention sent to another chat, `<clawchat:no-reply/>`,
|
|
25
|
+
* dispatch failure) leaves it behind; without this sweep the map would grow by
|
|
26
|
+
* one entry per such turn for the lifetime of the gateway.
|
|
27
|
+
*/
|
|
28
|
+
function sweepExpiredTerminalSends(nowMs: number): void {
|
|
29
|
+
for (const [recordKey, record] of terminalSends) {
|
|
30
|
+
if (record.expiresAt <= nowMs) terminalSends.delete(recordKey);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export function markTerminalClawChatSend(params: {
|
|
22
35
|
accountId: string;
|
|
23
36
|
chatId: string;
|
|
@@ -28,9 +41,11 @@ export function markTerminalClawChatSend(params: {
|
|
|
28
41
|
}): void {
|
|
29
42
|
const scopeId = params.scopeId ?? terminalSendScope.getStore();
|
|
30
43
|
if (!scopeId) return;
|
|
44
|
+
const nowMs = params.nowMs ?? Date.now();
|
|
45
|
+
sweepExpiredTerminalSends(nowMs);
|
|
31
46
|
terminalSends.set(key(params.accountId, params.chatId, scopeId), {
|
|
32
47
|
messageId: params.messageId,
|
|
33
|
-
expiresAt:
|
|
48
|
+
expiresAt: nowMs + (params.ttlMs ?? DEFAULT_TTL_MS),
|
|
34
49
|
scopeId,
|
|
35
50
|
});
|
|
36
51
|
}
|