@botcord/daemon 0.2.4 → 0.2.6
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/agent-discovery.d.ts +7 -3
- package/dist/agent-discovery.js +9 -1
- package/dist/agent-workspace.d.ts +62 -0
- package/dist/agent-workspace.js +140 -10
- package/dist/config.d.ts +49 -1
- package/dist/config.js +57 -1
- package/dist/control-channel.d.ts +1 -4
- package/dist/control-channel.js +1 -4
- package/dist/daemon-config-map.d.ts +29 -12
- package/dist/daemon-config-map.js +105 -8
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +52 -5
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.js +22 -1
- package/dist/gateway/cli-resolver.d.ts +34 -0
- package/dist/gateway/cli-resolver.js +74 -0
- package/dist/gateway/dispatcher.d.ts +66 -1
- package/dist/gateway/dispatcher.js +583 -56
- package/dist/gateway/gateway.d.ts +29 -1
- package/dist/gateway/gateway.js +10 -0
- package/dist/gateway/index.d.ts +2 -0
- package/dist/gateway/index.js +2 -0
- package/dist/gateway/policy-resolver.d.ts +57 -0
- package/dist/gateway/policy-resolver.js +123 -0
- package/dist/gateway/runtimes/acp-stream.d.ts +99 -0
- package/dist/gateway/runtimes/acp-stream.js +394 -0
- package/dist/gateway/runtimes/codex.js +7 -0
- package/dist/gateway/runtimes/hermes-agent.d.ts +83 -0
- package/dist/gateway/runtimes/hermes-agent.js +180 -0
- package/dist/gateway/runtimes/ndjson-stream.d.ts +7 -2
- package/dist/gateway/runtimes/ndjson-stream.js +16 -3
- package/dist/gateway/runtimes/openclaw-acp.d.ts +44 -0
- package/dist/gateway/runtimes/openclaw-acp.js +500 -0
- package/dist/gateway/runtimes/registry.d.ts +4 -0
- package/dist/gateway/runtimes/registry.js +22 -0
- package/dist/gateway/transcript-paths.d.ts +30 -0
- package/dist/gateway/transcript-paths.js +114 -0
- package/dist/gateway/transcript.d.ts +123 -0
- package/dist/gateway/transcript.js +147 -0
- package/dist/gateway/types.d.ts +31 -0
- package/dist/index.js +286 -27
- package/dist/mention-scan.d.ts +22 -0
- package/dist/mention-scan.js +35 -0
- package/dist/provision.d.ts +73 -3
- package/dist/provision.js +373 -12
- package/dist/system-context.d.ts +5 -4
- package/dist/system-context.js +35 -5
- package/dist/turn-text.js +20 -1
- package/dist/url-utils.d.ts +9 -0
- package/dist/url-utils.js +18 -0
- package/dist/user-auth.js +0 -2
- package/dist/working-memory.js +1 -1
- package/package.json +2 -1
- package/src/__tests__/agent-workspace.test.ts +93 -0
- package/src/__tests__/daemon-config-map.test.ts +79 -0
- package/src/__tests__/openclaw-acp.test.ts +234 -0
- package/src/__tests__/policy-resolver.test.ts +124 -0
- package/src/__tests__/policy-updated-handler.test.ts +144 -0
- package/src/__tests__/provision.test.ts +160 -0
- package/src/__tests__/system-context.test.ts +52 -0
- package/src/__tests__/url-utils.test.ts +37 -0
- package/src/agent-discovery.ts +12 -4
- package/src/agent-workspace.ts +173 -9
- package/src/config.ts +132 -4
- package/src/control-channel.ts +1 -4
- package/src/daemon-config-map.ts +156 -12
- package/src/daemon.ts +66 -5
- package/src/doctor.ts +49 -2
- package/src/gateway/__tests__/dispatcher.test.ts +440 -2
- package/src/gateway/__tests__/hermes-agent-adapter.test.ts +302 -0
- package/src/gateway/__tests__/transcript.test.ts +496 -0
- package/src/gateway/cli-resolver.ts +92 -0
- package/src/gateway/dispatcher.ts +681 -58
- package/src/gateway/gateway.ts +46 -0
- package/src/gateway/index.ts +25 -0
- package/src/gateway/policy-resolver.ts +171 -0
- package/src/gateway/runtimes/acp-stream.ts +535 -0
- package/src/gateway/runtimes/codex.ts +7 -0
- package/src/gateway/runtimes/hermes-agent.ts +206 -0
- package/src/gateway/runtimes/ndjson-stream.ts +16 -3
- package/src/gateway/runtimes/openclaw-acp.ts +606 -0
- package/src/gateway/runtimes/registry.ts +24 -0
- package/src/gateway/transcript-paths.ts +145 -0
- package/src/gateway/transcript.ts +300 -0
- package/src/gateway/types.ts +32 -0
- package/src/index.ts +295 -30
- package/src/mention-scan.ts +38 -0
- package/src/provision.ts +446 -20
- package/src/system-context.ts +41 -9
- package/src/turn-text.ts +22 -1
- package/src/url-utils.ts +17 -0
- package/src/user-auth.ts +0 -2
- package/src/working-memory.ts +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const FAST_PATH_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
4
|
+
const WIN_RESERVED = new Set([
|
|
5
|
+
"CON", "PRN", "AUX", "NUL",
|
|
6
|
+
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
|
7
|
+
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
|
8
|
+
]);
|
|
9
|
+
const MAX_LEN = 200;
|
|
10
|
+
const TRUNCATE_PREFIX = 191;
|
|
11
|
+
function sha256_8(raw) {
|
|
12
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 8);
|
|
13
|
+
}
|
|
14
|
+
function isControlOrNul(ch) {
|
|
15
|
+
return ch === 0 || ch < 0x20 || ch === 0x7f;
|
|
16
|
+
}
|
|
17
|
+
function isAllControl(raw) {
|
|
18
|
+
for (let i = 0; i < raw.length; i++) {
|
|
19
|
+
if (!isControlOrNul(raw.charCodeAt(i)))
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
function percentEncodeByte(byte) {
|
|
25
|
+
return "%" + byte.toString(16).toUpperCase().padStart(2, "0");
|
|
26
|
+
}
|
|
27
|
+
function isWhitelistByte(byte) {
|
|
28
|
+
// [A-Za-z0-9_-%] retained as literal
|
|
29
|
+
return ((byte >= 0x30 && byte <= 0x39) || // 0-9
|
|
30
|
+
(byte >= 0x41 && byte <= 0x5a) || // A-Z
|
|
31
|
+
(byte >= 0x61 && byte <= 0x7a) || // a-z
|
|
32
|
+
byte === 0x5f || // _
|
|
33
|
+
byte === 0x2d || // -
|
|
34
|
+
byte === 0x25 // % (kept literal — design §3.1)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function escapeRaw(raw) {
|
|
38
|
+
const bytes = Buffer.from(raw, "utf8");
|
|
39
|
+
let out = "";
|
|
40
|
+
for (const b of bytes) {
|
|
41
|
+
out += isWhitelistByte(b) ? String.fromCharCode(b) : percentEncodeByte(b);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Truncate an escaped string to exactly MAX_LEN chars without splitting a `%XX`
|
|
47
|
+
* sequence. Keep first TRUNCATE_PREFIX chars (rolled back if mid-`%XX`), then
|
|
48
|
+
* `_` + sha256-8(raw) so the total is always ≤ MAX_LEN.
|
|
49
|
+
*/
|
|
50
|
+
function truncateEscaped(escaped, raw) {
|
|
51
|
+
let cut = TRUNCATE_PREFIX;
|
|
52
|
+
// Roll back if cut sits inside a `%XX` sequence.
|
|
53
|
+
// A '%' at position cut-1 or cut-2 means the next 1 or 2 chars belong to it.
|
|
54
|
+
if (cut >= 1 && escaped[cut - 1] === "%")
|
|
55
|
+
cut -= 1;
|
|
56
|
+
else if (cut >= 2 && escaped[cut - 2] === "%")
|
|
57
|
+
cut -= 2;
|
|
58
|
+
const hash = sha256_8(raw);
|
|
59
|
+
return escaped.slice(0, cut) + "_" + hash;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Convert a raw ID into a filesystem-safe path segment.
|
|
63
|
+
*
|
|
64
|
+
* Order (must not be reordered — see design §3.1):
|
|
65
|
+
* 1. obviously invalid (empty / `.` / `..` / all control/NUL)
|
|
66
|
+
* → `_invalid_<sha256-8>`
|
|
67
|
+
* 2. Windows reserved name (CON/PRN/AUX/NUL/COM1-9/LPT1-9, case-insensitive)
|
|
68
|
+
* → `_win_<raw>`
|
|
69
|
+
* 3. fast path (`^[A-Za-z0-9_-]{1,128}$`) → return raw
|
|
70
|
+
* 4. percent-encode non-whitelist bytes; truncate at 200 chars without
|
|
71
|
+
* splitting a `%XX` (191 prefix + `_` + sha256-8)
|
|
72
|
+
*
|
|
73
|
+
* The original ID is always written into the transcript record itself; this
|
|
74
|
+
* helper only sanitizes the on-disk filename.
|
|
75
|
+
*/
|
|
76
|
+
export function safePathSegment(raw) {
|
|
77
|
+
// 1. obviously invalid
|
|
78
|
+
if (raw === "" || raw === "." || raw === ".." || isAllControl(raw)) {
|
|
79
|
+
return "_invalid_" + sha256_8(raw);
|
|
80
|
+
}
|
|
81
|
+
// 2. Windows reserved names — must run BEFORE fast path so `CON` is not
|
|
82
|
+
// leaked unchanged on case-insensitive filesystems.
|
|
83
|
+
if (WIN_RESERVED.has(raw.toUpperCase())) {
|
|
84
|
+
return "_win_" + raw;
|
|
85
|
+
}
|
|
86
|
+
// 3. fast path
|
|
87
|
+
if (FAST_PATH_RE.test(raw))
|
|
88
|
+
return raw;
|
|
89
|
+
// 4. escape + maybe truncate
|
|
90
|
+
const escaped = escapeRaw(raw);
|
|
91
|
+
if (escaped.length <= MAX_LEN)
|
|
92
|
+
return escaped;
|
|
93
|
+
return truncateEscaped(escaped, raw);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the on-disk transcript file for a given (agent, room, topic). Used
|
|
97
|
+
* by the writer AND the CLI subcommands so both look at the same file.
|
|
98
|
+
*
|
|
99
|
+
* Layout (design §3.1):
|
|
100
|
+
* <rootDir>/<agentId>/transcripts/<roomId>/<topicId|_default>.jsonl
|
|
101
|
+
*
|
|
102
|
+
* Where <rootDir> is typically `~/.botcord/agents`.
|
|
103
|
+
*/
|
|
104
|
+
export function transcriptFilePath(rootDir, agentId, roomId, topicId) {
|
|
105
|
+
return path.join(rootDir, safePathSegment(agentId), "transcripts", safePathSegment(roomId), (topicId === null ? "_default" : safePathSegment(topicId)) + ".jsonl");
|
|
106
|
+
}
|
|
107
|
+
/** Directory holding a (agent, room) pair's transcript files. */
|
|
108
|
+
export function transcriptRoomDir(rootDir, agentId, roomId) {
|
|
109
|
+
return path.join(rootDir, safePathSegment(agentId), "transcripts", safePathSegment(roomId));
|
|
110
|
+
}
|
|
111
|
+
/** Directory holding all transcript rooms for a single agent. */
|
|
112
|
+
export function transcriptAgentRoot(rootDir, agentId) {
|
|
113
|
+
return path.join(rootDir, safePathSegment(agentId), "transcripts");
|
|
114
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { GatewayLogger } from "./log.js";
|
|
2
|
+
/**
|
|
3
|
+
* Soft cap on a single textual field (`text` / `composedText` / `finalText`).
|
|
4
|
+
* Anything longer is truncated and `truncated.<field>` set to `true`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const TRANSCRIPT_TEXT_LIMIT: number;
|
|
7
|
+
/** Soft cap on a single transcript file before rotation. */
|
|
8
|
+
export declare const TRANSCRIPT_FILE_LIMIT: number;
|
|
9
|
+
/** Default root directory for per-agent transcript trees. */
|
|
10
|
+
export declare function defaultTranscriptRoot(): string;
|
|
11
|
+
export type TranscriptRecordKind = "inbound" | "dispatched" | "compose_failed" | "outbound" | "turn_error" | "attention_skipped" | "dropped";
|
|
12
|
+
export interface TranscriptRecordBase {
|
|
13
|
+
ts: string;
|
|
14
|
+
kind: TranscriptRecordKind;
|
|
15
|
+
turnId: string;
|
|
16
|
+
agentId: string;
|
|
17
|
+
roomId: string;
|
|
18
|
+
topicId: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface TranscriptSenderInfo {
|
|
21
|
+
id: string;
|
|
22
|
+
kind: "user" | "agent" | "system";
|
|
23
|
+
name?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface InboundTranscriptRecord extends TranscriptRecordBase {
|
|
26
|
+
kind: "inbound";
|
|
27
|
+
messageId: string;
|
|
28
|
+
sender: TranscriptSenderInfo;
|
|
29
|
+
text: string;
|
|
30
|
+
rawBatchEntries?: number;
|
|
31
|
+
trace?: {
|
|
32
|
+
id: string;
|
|
33
|
+
streamable?: boolean;
|
|
34
|
+
};
|
|
35
|
+
truncated?: {
|
|
36
|
+
text?: true;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export interface DispatchedTranscriptRecord extends TranscriptRecordBase {
|
|
40
|
+
kind: "dispatched";
|
|
41
|
+
composedText: string;
|
|
42
|
+
mergedFromTurnIds?: string[];
|
|
43
|
+
runtime: string;
|
|
44
|
+
truncated?: {
|
|
45
|
+
composedText?: true;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface ComposeFailedTranscriptRecord extends TranscriptRecordBase {
|
|
49
|
+
kind: "compose_failed";
|
|
50
|
+
error: string;
|
|
51
|
+
fallback: "raw_text";
|
|
52
|
+
}
|
|
53
|
+
export type DeliveryStatus = "delivered" | "gated_non_owner_chat" | "empty_text" | "send_failed";
|
|
54
|
+
export interface TranscriptBlockSummary {
|
|
55
|
+
type: string;
|
|
56
|
+
chars?: number;
|
|
57
|
+
name?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface OutboundTranscriptRecord extends TranscriptRecordBase {
|
|
60
|
+
kind: "outbound";
|
|
61
|
+
runtime: string;
|
|
62
|
+
runtimeSessionId?: string | null;
|
|
63
|
+
durationMs: number;
|
|
64
|
+
costUsd?: number;
|
|
65
|
+
finalText: string;
|
|
66
|
+
deliveryStatus: DeliveryStatus;
|
|
67
|
+
deliveryReason?: string | null;
|
|
68
|
+
blocks?: TranscriptBlockSummary[];
|
|
69
|
+
truncated?: {
|
|
70
|
+
finalText?: true;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface TurnErrorTranscriptRecord extends TranscriptRecordBase {
|
|
74
|
+
kind: "turn_error";
|
|
75
|
+
phase: "runtime" | "timeout";
|
|
76
|
+
error: string;
|
|
77
|
+
durationMs: number;
|
|
78
|
+
}
|
|
79
|
+
export interface AttentionSkippedTranscriptRecord extends TranscriptRecordBase {
|
|
80
|
+
kind: "attention_skipped";
|
|
81
|
+
reason: string;
|
|
82
|
+
}
|
|
83
|
+
export type DroppedReason = "batch_merged" | "queue_cancel_previous" | "queue_overflow";
|
|
84
|
+
export interface DroppedTranscriptRecord extends TranscriptRecordBase {
|
|
85
|
+
kind: "dropped";
|
|
86
|
+
reason: DroppedReason;
|
|
87
|
+
supersededBy?: string | null;
|
|
88
|
+
}
|
|
89
|
+
export type TranscriptRecord = InboundTranscriptRecord | DispatchedTranscriptRecord | ComposeFailedTranscriptRecord | OutboundTranscriptRecord | TurnErrorTranscriptRecord | AttentionSkippedTranscriptRecord | DroppedTranscriptRecord;
|
|
90
|
+
/**
|
|
91
|
+
* Truncate `value` to TRANSCRIPT_TEXT_LIMIT chars. Returns the (possibly
|
|
92
|
+
* truncated) text and whether truncation occurred. Surrogate-pair aware: if
|
|
93
|
+
* the cut would split a pair, step back one char.
|
|
94
|
+
*/
|
|
95
|
+
export declare function truncateTextField(value: string): {
|
|
96
|
+
text: string;
|
|
97
|
+
truncated: boolean;
|
|
98
|
+
};
|
|
99
|
+
export interface TranscriptWriter {
|
|
100
|
+
/** Append a record. Failures are logged and swallowed. */
|
|
101
|
+
write(rec: TranscriptRecord): void;
|
|
102
|
+
/** Whether persistence is on. CLI / tests may read this. */
|
|
103
|
+
readonly enabled: boolean;
|
|
104
|
+
/** Root directory used for path resolution. */
|
|
105
|
+
readonly rootDir: string;
|
|
106
|
+
}
|
|
107
|
+
export interface CreateTranscriptWriterOptions {
|
|
108
|
+
/** Defaults to `~/.botcord/agents`. */
|
|
109
|
+
rootDir?: string;
|
|
110
|
+
log: GatewayLogger;
|
|
111
|
+
/** Defaults to `false` — see design §6 (default-off). */
|
|
112
|
+
enabled?: boolean;
|
|
113
|
+
/** Override file rotation threshold (bytes). Defaults to TRANSCRIPT_FILE_LIMIT. */
|
|
114
|
+
maxFileBytes?: number;
|
|
115
|
+
}
|
|
116
|
+
export declare function createTranscriptWriter(opts: CreateTranscriptWriterOptions): TranscriptWriter;
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the tri-state enable flag (env wins; otherwise config). See design §5.
|
|
119
|
+
* - env === "1" → true (force on)
|
|
120
|
+
* - env === "0" → false (force off)
|
|
121
|
+
* - any other / unset → fall back to `configEnabled`
|
|
122
|
+
*/
|
|
123
|
+
export declare function resolveTranscriptEnabled(envVal: string | undefined, configEnabled: boolean): boolean;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, renameSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { transcriptFilePath } from "./transcript-paths.js";
|
|
5
|
+
/**
|
|
6
|
+
* Soft cap on a single textual field (`text` / `composedText` / `finalText`).
|
|
7
|
+
* Anything longer is truncated and `truncated.<field>` set to `true`.
|
|
8
|
+
*/
|
|
9
|
+
export const TRANSCRIPT_TEXT_LIMIT = 32 * 1024;
|
|
10
|
+
/** Soft cap on a single transcript file before rotation. */
|
|
11
|
+
export const TRANSCRIPT_FILE_LIMIT = 8 * 1024 * 1024;
|
|
12
|
+
/** Default root directory for per-agent transcript trees. */
|
|
13
|
+
export function defaultTranscriptRoot() {
|
|
14
|
+
return path.join(homedir(), ".botcord", "agents");
|
|
15
|
+
}
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Truncation helper
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Truncate `value` to TRANSCRIPT_TEXT_LIMIT chars. Returns the (possibly
|
|
21
|
+
* truncated) text and whether truncation occurred. Surrogate-pair aware: if
|
|
22
|
+
* the cut would split a pair, step back one char.
|
|
23
|
+
*/
|
|
24
|
+
export function truncateTextField(value) {
|
|
25
|
+
if (value.length <= TRANSCRIPT_TEXT_LIMIT)
|
|
26
|
+
return { text: value, truncated: false };
|
|
27
|
+
let cut = TRANSCRIPT_TEXT_LIMIT;
|
|
28
|
+
const code = value.charCodeAt(cut - 1);
|
|
29
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
30
|
+
cut -= 1; // mid-surrogate
|
|
31
|
+
return { text: value.slice(0, cut), truncated: true };
|
|
32
|
+
}
|
|
33
|
+
class NoopTranscriptWriter {
|
|
34
|
+
enabled = false;
|
|
35
|
+
rootDir;
|
|
36
|
+
constructor(rootDir) {
|
|
37
|
+
this.rootDir = rootDir;
|
|
38
|
+
}
|
|
39
|
+
write(_rec) {
|
|
40
|
+
// intentionally empty
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
class FsTranscriptWriter {
|
|
44
|
+
enabled = true;
|
|
45
|
+
rootDir;
|
|
46
|
+
log;
|
|
47
|
+
maxFileBytes;
|
|
48
|
+
fileMeta = new Map();
|
|
49
|
+
firstWriteAnnounced = false;
|
|
50
|
+
constructor(rootDir, log, maxFileBytes) {
|
|
51
|
+
this.rootDir = rootDir;
|
|
52
|
+
this.log = log;
|
|
53
|
+
this.maxFileBytes = maxFileBytes;
|
|
54
|
+
}
|
|
55
|
+
write(rec) {
|
|
56
|
+
try {
|
|
57
|
+
const file = transcriptFilePath(this.rootDir, rec.agentId, rec.roomId, rec.topicId);
|
|
58
|
+
const dir = path.dirname(file);
|
|
59
|
+
try {
|
|
60
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// best-effort — appendFileSync below will surface a real error
|
|
64
|
+
}
|
|
65
|
+
const line = JSON.stringify(rec) + "\n";
|
|
66
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
67
|
+
// Rotate before appending if the existing file would exceed the cap.
|
|
68
|
+
const meta = this.statFile(file);
|
|
69
|
+
if (meta.size > 0 && meta.size + bytes > this.maxFileBytes) {
|
|
70
|
+
this.rotate(file);
|
|
71
|
+
this.fileMeta.delete(file);
|
|
72
|
+
}
|
|
73
|
+
appendFileSync(file, line, { mode: 0o600 });
|
|
74
|
+
const cur = this.fileMeta.get(file) ?? { size: meta.size };
|
|
75
|
+
cur.size = (cur.size || 0) + bytes;
|
|
76
|
+
this.fileMeta.set(file, cur);
|
|
77
|
+
if (!this.firstWriteAnnounced) {
|
|
78
|
+
this.firstWriteAnnounced = true;
|
|
79
|
+
this.log.info("transcript enabled", { dir: this.rootDir });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
this.log.warn("transcript: write failed", {
|
|
84
|
+
kind: rec.kind,
|
|
85
|
+
turnId: rec.turnId,
|
|
86
|
+
error: err instanceof Error ? err.message : String(err),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
statFile(file) {
|
|
91
|
+
const cached = this.fileMeta.get(file);
|
|
92
|
+
if (cached)
|
|
93
|
+
return cached;
|
|
94
|
+
try {
|
|
95
|
+
const st = statSync(file);
|
|
96
|
+
const meta = { size: st.size };
|
|
97
|
+
this.fileMeta.set(file, meta);
|
|
98
|
+
return meta;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
const meta = { size: 0 };
|
|
102
|
+
this.fileMeta.set(file, meta);
|
|
103
|
+
return meta;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
rotate(file) {
|
|
107
|
+
const stamp = formatStamp(new Date());
|
|
108
|
+
const ext = ".jsonl";
|
|
109
|
+
const base = file.endsWith(ext) ? file.slice(0, -ext.length) : file;
|
|
110
|
+
const rotated = `${base}.${stamp}${ext}`;
|
|
111
|
+
try {
|
|
112
|
+
renameSync(file, rotated);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
this.log.warn("transcript: rotate failed", {
|
|
116
|
+
file,
|
|
117
|
+
error: err instanceof Error ? err.message : String(err),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export function createTranscriptWriter(opts) {
|
|
123
|
+
const rootDir = opts.rootDir ?? defaultTranscriptRoot();
|
|
124
|
+
const enabled = opts.enabled ?? false;
|
|
125
|
+
if (!enabled)
|
|
126
|
+
return new NoopTranscriptWriter(rootDir);
|
|
127
|
+
const maxBytes = opts.maxFileBytes ?? TRANSCRIPT_FILE_LIMIT;
|
|
128
|
+
return new FsTranscriptWriter(rootDir, opts.log, maxBytes);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the tri-state enable flag (env wins; otherwise config). See design §5.
|
|
132
|
+
* - env === "1" → true (force on)
|
|
133
|
+
* - env === "0" → false (force off)
|
|
134
|
+
* - any other / unset → fall back to `configEnabled`
|
|
135
|
+
*/
|
|
136
|
+
export function resolveTranscriptEnabled(envVal, configEnabled) {
|
|
137
|
+
if (envVal === "1")
|
|
138
|
+
return true;
|
|
139
|
+
if (envVal === "0")
|
|
140
|
+
return false;
|
|
141
|
+
return configEnabled;
|
|
142
|
+
}
|
|
143
|
+
function formatStamp(d) {
|
|
144
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
145
|
+
return (`${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}` +
|
|
146
|
+
`-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}`);
|
|
147
|
+
}
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -13,6 +13,19 @@ export interface RouteMatch {
|
|
|
13
13
|
export type QueueMode = "serial" | "cancel-previous";
|
|
14
14
|
/** Source-based trust tier used by runtimes to pick default permission flags. */
|
|
15
15
|
export type TrustLevel = "owner" | "trusted" | "public";
|
|
16
|
+
/**
|
|
17
|
+
* Resolved OpenClaw gateway endpoint for a route. Built eagerly in
|
|
18
|
+
* `toGatewayConfig` from the `DaemonConfig.openclawGateways` registry plus the
|
|
19
|
+
* `RouteRule.gateway` / `openclawAgent` choice — the dispatcher never needs
|
|
20
|
+
* to re-query the registry. `name` is preserved purely for logging/snapshot.
|
|
21
|
+
*/
|
|
22
|
+
export interface ResolvedOpenclawGateway {
|
|
23
|
+
name: string;
|
|
24
|
+
url: string;
|
|
25
|
+
token?: string;
|
|
26
|
+
/** OpenClaw agent profile, with the route override already applied. */
|
|
27
|
+
openclawAgent?: string;
|
|
28
|
+
}
|
|
16
29
|
/** Declarative route entry selecting the runtime and execution flags for matched messages. */
|
|
17
30
|
export interface GatewayRoute {
|
|
18
31
|
match?: RouteMatch;
|
|
@@ -21,6 +34,8 @@ export interface GatewayRoute {
|
|
|
21
34
|
extraArgs?: string[];
|
|
22
35
|
queueMode?: QueueMode;
|
|
23
36
|
trustLevel?: TrustLevel;
|
|
37
|
+
/** Required when `runtime === "openclaw-acp"`. Resolved at config-load time. */
|
|
38
|
+
gateway?: ResolvedOpenclawGateway;
|
|
24
39
|
}
|
|
25
40
|
/**
|
|
26
41
|
* Per-channel configuration entry. Channel-specific extras (e.g. BotCord
|
|
@@ -214,6 +229,15 @@ export interface RuntimeRunOptions {
|
|
|
214
229
|
* per-agent `CODEX_HOME` carrying the AGENTS.md that injects systemContext.
|
|
215
230
|
*/
|
|
216
231
|
accountId: string;
|
|
232
|
+
/**
|
|
233
|
+
* Hub URL the owning agent is registered against. Forwarded to runtimes
|
|
234
|
+
* so spawned CLI subprocesses can target the correct hub via
|
|
235
|
+
* `BOTCORD_HUB` (see `cli-resolver.buildCliEnv`). Optional because the
|
|
236
|
+
* dispatcher cannot always resolve a per-agent hub (e.g. for agents
|
|
237
|
+
* provisioned after boot); when unset, runtimes leave `BOTCORD_HUB`
|
|
238
|
+
* unspecified and the bundled CLI falls back to its own default.
|
|
239
|
+
*/
|
|
240
|
+
hubUrl?: string;
|
|
217
241
|
signal: AbortSignal;
|
|
218
242
|
extraArgs?: string[];
|
|
219
243
|
trustLevel: TrustLevel;
|
|
@@ -223,6 +247,13 @@ export interface RuntimeRunOptions {
|
|
|
223
247
|
context?: Record<string, unknown>;
|
|
224
248
|
/** Called for every parsed block while the turn is in progress. */
|
|
225
249
|
onBlock?: (block: StreamBlock) => void;
|
|
250
|
+
/**
|
|
251
|
+
* External service endpoint required by some runtimes (first user:
|
|
252
|
+
* openclaw-acp). Resolved at config-load time and passed through here per
|
|
253
|
+
* call — runtime factories do not see it. Mirrors the `hubUrl` precedent of
|
|
254
|
+
* lifting service URLs out of `extraArgs` into typed first-class fields.
|
|
255
|
+
*/
|
|
256
|
+
gateway?: ResolvedOpenclawGateway;
|
|
226
257
|
}
|
|
227
258
|
/** Result returned by a runtime adapter after a turn completes. */
|
|
228
259
|
export interface RuntimeRunResult {
|