@loggie-ai/openclaw-plugin 0.1.0 → 0.1.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/README.md +9 -4
- package/dist/setup-entry.d.ts +6 -0
- package/dist/src/channel.d.ts +6 -0
- package/dist/src/cursor-store.d.ts +8 -1
- package/dist/src/cursor-store.js +73 -2
- package/dist/src/monitor/connection.js +40 -2
- package/dist/src/monitor/event-handler.d.ts +14 -0
- package/dist/src/monitor/event-handler.js +35 -2
- package/dist/src/monitor/transcript-dispatch.js +50 -1
- package/dist/src/status.d.ts +12 -0
- package/dist/src/status.js +12 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/cursor-store.ts +103 -8
- package/src/monitor/connection.ts +45 -3
- package/src/monitor/event-handler.ts +46 -8
- package/src/monitor/transcript-dispatch.ts +90 -1
- package/src/status.ts +18 -0
package/README.md
CHANGED
|
@@ -15,7 +15,8 @@ OpenClaw inbound channel turn for the configured agent.
|
|
|
15
15
|
3. The monitor opens a WebSocket to `baseUrl + socketPath`, authenticating with
|
|
16
16
|
either `Authorization: Bearer <token>` or `x-api-key: <token>`.
|
|
17
17
|
4. Before opening the socket, the plugin loads the durable cursor from OpenClaw
|
|
18
|
-
runtime state
|
|
18
|
+
trusted runtime state when available, otherwise from the file-backed cursor
|
|
19
|
+
store, and appends it to the URL as `?cursor=<lastCursor>`.
|
|
19
20
|
5. Loggie sends event envelopes. The plugin validates the envelope, skips
|
|
20
21
|
duplicates or cursor regressions, and processes `meeting.transcript.ready`.
|
|
21
22
|
6. If the event includes `payload.detailPath`, the plugin fetches that detail
|
|
@@ -144,9 +145,13 @@ Session identity is deterministic:
|
|
|
144
145
|
- Embedded run session id:
|
|
145
146
|
`loggie-<accountId>-<meetingScheduleId|meetingId|eventId>`
|
|
146
147
|
|
|
147
|
-
The cursor store is
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
The cursor store is always durable. Trusted OpenClaw installs use
|
|
149
|
+
`runtime.state.openKeyedStore({ namespace: "loggie-cursors" })`. Plain external
|
|
150
|
+
npm installs that do not receive the trusted runtime state API fall back to a
|
|
151
|
+
file-backed store at `runtime.state.resolveStateDir()/loggie-cursors.json` when
|
|
152
|
+
OpenClaw exposes a state directory, or `~/.openclaw/state/loggie-cursors.json`
|
|
153
|
+
otherwise. Startup does not fail solely because trusted state is unavailable,
|
|
154
|
+
and the fallback still persists replay cursors across gateway restarts.
|
|
150
155
|
|
|
151
156
|
The plugin keeps a bounded set of recently seen event ids and skips:
|
|
152
157
|
|
package/dist/setup-entry.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ declare const _default: {
|
|
|
21
21
|
connected: {};
|
|
22
22
|
authenticated: {};
|
|
23
23
|
caughtUp: {};
|
|
24
|
+
cursorStoreMode: {} | null;
|
|
25
|
+
cursorPersistenceFailed: {};
|
|
26
|
+
lastCursorPersistenceAt: {} | null;
|
|
27
|
+
lastCursorPersistenceError: {} | null;
|
|
28
|
+
lastCursorPersistenceEventId: {} | null;
|
|
29
|
+
lastCursorPersistenceCursor: {} | null;
|
|
24
30
|
lastEventId: {} | null;
|
|
25
31
|
lastCursor: {} | null;
|
|
26
32
|
lastSequence: {} | null;
|
package/dist/src/channel.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ export declare const loggiePlugin: {
|
|
|
22
22
|
connected: {};
|
|
23
23
|
authenticated: {};
|
|
24
24
|
caughtUp: {};
|
|
25
|
+
cursorStoreMode: {} | null;
|
|
26
|
+
cursorPersistenceFailed: {};
|
|
27
|
+
lastCursorPersistenceAt: {} | null;
|
|
28
|
+
lastCursorPersistenceError: {} | null;
|
|
29
|
+
lastCursorPersistenceEventId: {} | null;
|
|
30
|
+
lastCursorPersistenceCursor: {} | null;
|
|
25
31
|
lastEventId: {} | null;
|
|
26
32
|
lastCursor: {} | null;
|
|
27
33
|
lastSequence: {} | null;
|
|
@@ -37,9 +37,14 @@ export type LoggieCursorDecision = {
|
|
|
37
37
|
reason: "duplicate_event" | "cursor_regression";
|
|
38
38
|
};
|
|
39
39
|
export type LoggieCursorStore = {
|
|
40
|
+
readonly mode?: LoggieCursorStoreMode;
|
|
40
41
|
load(accountId: string, agentProfileId: string): Promise<LoggieCursorRecord | undefined>;
|
|
41
42
|
save(record: LoggieCursorRecord): Promise<void>;
|
|
42
43
|
};
|
|
44
|
+
export type LoggieCursorStoreMode = "trusted" | "file";
|
|
45
|
+
export type RuntimeCursorStoreOptions = {
|
|
46
|
+
warn?: (message: string) => void;
|
|
47
|
+
};
|
|
43
48
|
export declare function createEmptyCursorRecord(params: {
|
|
44
49
|
accountId: string;
|
|
45
50
|
agentProfileId: string;
|
|
@@ -73,8 +78,10 @@ export declare function deadLetterEvent(params: {
|
|
|
73
78
|
now?: Date;
|
|
74
79
|
}): LoggieCursorRecord;
|
|
75
80
|
export declare function createMemoryCursorStore(initial?: LoggieCursorRecord[]): LoggieCursorStore;
|
|
81
|
+
export declare function createFileCursorStore(filePath?: string): LoggieCursorStore;
|
|
76
82
|
export declare function createRuntimeCursorStore(runtime: {
|
|
77
83
|
state?: {
|
|
84
|
+
resolveStateDir?: () => string;
|
|
78
85
|
openKeyedStore?: <T>(opts: {
|
|
79
86
|
namespace: string;
|
|
80
87
|
maxEntries: number;
|
|
@@ -83,4 +90,4 @@ export declare function createRuntimeCursorStore(runtime: {
|
|
|
83
90
|
register(key: string, value: T): Promise<void>;
|
|
84
91
|
};
|
|
85
92
|
};
|
|
86
|
-
}): LoggieCursorStore;
|
|
93
|
+
}, options?: RuntimeCursorStoreOptions): LoggieCursorStore;
|
package/dist/src/cursor-store.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
1
4
|
const MAX_SEEN_EVENT_IDS = 500;
|
|
2
5
|
const MAX_FAILED_EVENTS = 50;
|
|
3
6
|
const MAX_DEAD_LETTERED_EVENTS = 100;
|
|
7
|
+
const warnedFileCursorStorePaths = new Set();
|
|
4
8
|
export function createEmptyCursorRecord(params) {
|
|
5
9
|
return {
|
|
6
10
|
accountId: params.accountId,
|
|
@@ -111,19 +115,86 @@ export function createMemoryCursorStore(initial) {
|
|
|
111
115
|
},
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
|
-
export function
|
|
118
|
+
export function createFileCursorStore(filePath = defaultCursorStorePath()) {
|
|
119
|
+
let pending = Promise.resolve();
|
|
120
|
+
async function withFileLock(operation) {
|
|
121
|
+
const result = pending.then(operation, operation);
|
|
122
|
+
pending = result.then(() => undefined, () => undefined);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
mode: "file",
|
|
127
|
+
load(accountId, agentProfileId) {
|
|
128
|
+
return withFileLock(async () => {
|
|
129
|
+
const records = await readCursorFile(filePath);
|
|
130
|
+
return records[cursorKey(accountId, agentProfileId)];
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
save(record) {
|
|
134
|
+
return withFileLock(async () => {
|
|
135
|
+
const records = await readCursorFile(filePath);
|
|
136
|
+
records[cursorKey(record.accountId, record.agentProfileId)] = record;
|
|
137
|
+
await writeCursorFile(filePath, records);
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function createRuntimeCursorStore(runtime, options = {}) {
|
|
115
143
|
const store = runtime.state?.openKeyedStore?.({
|
|
116
144
|
namespace: "loggie-cursors",
|
|
117
145
|
maxEntries: 1000,
|
|
118
146
|
});
|
|
119
147
|
if (!store) {
|
|
120
|
-
|
|
148
|
+
const stateDir = runtime.state?.resolveStateDir?.();
|
|
149
|
+
const filePath = stateDir ? join(stateDir, "loggie-cursors.json") : defaultCursorStorePath();
|
|
150
|
+
warnFileCursorStoreFallbackOnce(filePath, options.warn);
|
|
151
|
+
return createFileCursorStore(filePath);
|
|
121
152
|
}
|
|
122
153
|
return {
|
|
154
|
+
mode: "trusted",
|
|
123
155
|
load: (accountId, agentProfileId) => store.lookup(cursorKey(accountId, agentProfileId)),
|
|
124
156
|
save: (record) => store.register(cursorKey(record.accountId, record.agentProfileId), record),
|
|
125
157
|
};
|
|
126
158
|
}
|
|
159
|
+
function defaultCursorStorePath() {
|
|
160
|
+
return join(homedir(), ".openclaw", "state", "loggie-cursors.json");
|
|
161
|
+
}
|
|
162
|
+
function warnFileCursorStoreFallbackOnce(filePath, warn) {
|
|
163
|
+
if (!warn || warnedFileCursorStorePaths.has(filePath)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
warnedFileCursorStorePaths.add(filePath);
|
|
167
|
+
warn(`Loggie trusted runtime cursor store is unavailable; using file-backed cursor store at ${filePath}. Cursor durability now depends on filesystem access to that path.`);
|
|
168
|
+
}
|
|
169
|
+
async function readCursorFile(filePath) {
|
|
170
|
+
try {
|
|
171
|
+
const contents = await fs.readFile(filePath, "utf8");
|
|
172
|
+
const parsed = JSON.parse(contents);
|
|
173
|
+
if (isCursorFile(parsed)) {
|
|
174
|
+
return parsed;
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`Loggie cursor store file has invalid JSON shape: ${filePath}`);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function writeCursorFile(filePath, records) {
|
|
186
|
+
const directory = dirname(filePath);
|
|
187
|
+
await fs.mkdir(directory, { recursive: true });
|
|
188
|
+
const tempPath = join(directory, `.loggie-cursors.${process.pid}.${Date.now()}.tmp`);
|
|
189
|
+
await fs.writeFile(tempPath, `${JSON.stringify(records, null, 2)}\n`, "utf8");
|
|
190
|
+
await fs.rename(tempPath, filePath);
|
|
191
|
+
}
|
|
192
|
+
function isCursorFile(value) {
|
|
193
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
194
|
+
}
|
|
195
|
+
function isNodeError(error) {
|
|
196
|
+
return error instanceof Error && "code" in error;
|
|
197
|
+
}
|
|
127
198
|
function cursorKey(accountId, agentProfileId) {
|
|
128
199
|
return `${accountId}:${agentProfileId}`;
|
|
129
200
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRuntimeCursorStore } from "../cursor-store.js";
|
|
2
2
|
import { buildLoggieAuthHeaders, buildLoggiePingMessage, buildLoggieResumeMessage, buildLoggieWebSocketUrl, decodeLoggieSocketMessage, defaultLoggieSocketFactory, } from "../loggie-client.js";
|
|
3
3
|
import { createOpenClawTranscriptDispatcher } from "./transcript-dispatch.js";
|
|
4
|
-
import { handleLoggieEvent } from "./event-handler.js";
|
|
4
|
+
import { handleLoggieEvent, isLoggieCursorPersistenceError } from "./event-handler.js";
|
|
5
5
|
export async function monitorLoggieAccount(params) {
|
|
6
6
|
if (!params.account.enabled) {
|
|
7
7
|
params.log?.info?.(`[${params.account.accountId}] Loggie account disabled`);
|
|
@@ -13,7 +13,7 @@ export async function monitorLoggieAccount(params) {
|
|
|
13
13
|
if (!params.channelRuntime && !params.dispatcher) {
|
|
14
14
|
throw new Error("Loggie channelRuntime is required for transcript dispatch");
|
|
15
15
|
}
|
|
16
|
-
const cursorStore = params.cursorStore ?? createRuntimeCursorStore(params.runtime);
|
|
16
|
+
const cursorStore = params.cursorStore ?? createRuntimeCursorStore(params.runtime, { warn: params.log?.warn });
|
|
17
17
|
const dispatcher = params.dispatcher ??
|
|
18
18
|
createOpenClawTranscriptDispatcher({
|
|
19
19
|
account: params.account,
|
|
@@ -38,6 +38,24 @@ export async function monitorLoggieAccount(params) {
|
|
|
38
38
|
if (params.abortSignal?.aborted) {
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
+
if (isLoggieCursorPersistenceError(error)) {
|
|
42
|
+
const failedAt = Date.now();
|
|
43
|
+
params.setStatus?.({
|
|
44
|
+
...(params.getStatus?.() ?? { accountId: params.account.accountId }),
|
|
45
|
+
running: false,
|
|
46
|
+
connected: false,
|
|
47
|
+
authenticated: false,
|
|
48
|
+
...cursorStoreStatus(cursorStore.mode),
|
|
49
|
+
cursorPersistenceFailed: true,
|
|
50
|
+
lastCursorPersistenceAt: failedAt,
|
|
51
|
+
lastCursorPersistenceError: error.message,
|
|
52
|
+
lastCursorPersistenceEventId: error.eventId,
|
|
53
|
+
lastCursorPersistenceCursor: error.cursor ?? null,
|
|
54
|
+
lastError: error.message,
|
|
55
|
+
});
|
|
56
|
+
params.log?.warn?.(`[${params.account.accountId}] Loggie cursor persistence failed; stopping monitor to avoid duplicate transcript dispatch: ${error.message}`);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
41
59
|
attempts += 1;
|
|
42
60
|
const delayMs = Math.min(params.account.reconnect.maxMs, params.account.reconnect.minMs * 2 ** Math.max(0, attempts - 1));
|
|
43
61
|
params.setStatus?.({
|
|
@@ -46,6 +64,8 @@ export async function monitorLoggieAccount(params) {
|
|
|
46
64
|
connected: false,
|
|
47
65
|
authenticated: false,
|
|
48
66
|
reconnectAttempts: attempts,
|
|
67
|
+
...cursorStoreStatus(cursorStore.mode),
|
|
68
|
+
...cursorPersistenceOkStatus(),
|
|
49
69
|
lastError: error instanceof Error ? error.message : String(error),
|
|
50
70
|
});
|
|
51
71
|
params.log?.warn?.(`[${params.account.accountId}] Loggie socket failed; reconnecting in ${delayMs}ms`);
|
|
@@ -64,6 +84,8 @@ export async function runLoggieSocketOnce(params) {
|
|
|
64
84
|
running: true,
|
|
65
85
|
connected: false,
|
|
66
86
|
authenticated: false,
|
|
87
|
+
...cursorStoreStatus(params.cursorStore.mode),
|
|
88
|
+
...cursorPersistenceOkStatus(),
|
|
67
89
|
lastStartAt: Date.now(),
|
|
68
90
|
});
|
|
69
91
|
await new Promise((resolve, reject) => {
|
|
@@ -145,6 +167,8 @@ export async function runLoggieSocketOnce(params) {
|
|
|
145
167
|
connected: true,
|
|
146
168
|
authenticated: true,
|
|
147
169
|
reconnectAttempts: 0,
|
|
170
|
+
...cursorStoreStatus(params.cursorStore.mode),
|
|
171
|
+
...cursorPersistenceOkStatus(),
|
|
148
172
|
lastConnectedAt: connectedAt,
|
|
149
173
|
lastError: null,
|
|
150
174
|
});
|
|
@@ -211,6 +235,8 @@ export async function runLoggieSocketOnce(params) {
|
|
|
211
235
|
connected: true,
|
|
212
236
|
authenticated: true,
|
|
213
237
|
caughtUp: true,
|
|
238
|
+
...cursorStoreStatus(params.cursorStore.mode),
|
|
239
|
+
...cursorPersistenceOkStatus(),
|
|
214
240
|
lastEventAt: eventAt,
|
|
215
241
|
...(result.status === "dispatched" ? { lastDispatchAt: eventAt } : {}),
|
|
216
242
|
...(deadLettered && result.event
|
|
@@ -253,6 +279,18 @@ export async function runLoggieSocketOnce(params) {
|
|
|
253
279
|
});
|
|
254
280
|
});
|
|
255
281
|
}
|
|
282
|
+
function cursorStoreStatus(mode) {
|
|
283
|
+
return mode ? { cursorStoreMode: mode } : {};
|
|
284
|
+
}
|
|
285
|
+
function cursorPersistenceOkStatus() {
|
|
286
|
+
return {
|
|
287
|
+
cursorPersistenceFailed: false,
|
|
288
|
+
lastCursorPersistenceAt: null,
|
|
289
|
+
lastCursorPersistenceError: null,
|
|
290
|
+
lastCursorPersistenceEventId: null,
|
|
291
|
+
lastCursorPersistenceCursor: null,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
256
294
|
function parseControlMessage(value) {
|
|
257
295
|
if (value === null || typeof value !== "object" || !("type" in value)) {
|
|
258
296
|
return { type: "none" };
|
|
@@ -18,6 +18,20 @@ export type LoggieEventHandleResult = {
|
|
|
18
18
|
reason: string;
|
|
19
19
|
retryable: boolean;
|
|
20
20
|
};
|
|
21
|
+
export declare class LoggieCursorPersistenceError extends Error {
|
|
22
|
+
readonly eventId: string;
|
|
23
|
+
readonly cursor?: string;
|
|
24
|
+
readonly accountId: string;
|
|
25
|
+
readonly agentProfileId: string;
|
|
26
|
+
readonly phase: "post_dispatch";
|
|
27
|
+
constructor(params: {
|
|
28
|
+
event: LoggieEventEnvelope;
|
|
29
|
+
account: ResolvedLoggieAccount;
|
|
30
|
+
phase: "post_dispatch";
|
|
31
|
+
cause: unknown;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export declare function isLoggieCursorPersistenceError(error: unknown): error is LoggieCursorPersistenceError;
|
|
21
35
|
export declare function handleLoggieEvent(params: {
|
|
22
36
|
raw: unknown;
|
|
23
37
|
account: ResolvedLoggieAccount;
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { advanceCursor, deadLetterEvent, decideCursor, recordEventFailure, } from "../cursor-store.js";
|
|
2
2
|
import { isTranscriptReadyEvent, parseLoggieEventEnvelope, } from "../event-types.js";
|
|
3
3
|
const MAX_EVENT_DISPATCH_ATTEMPTS = 3;
|
|
4
|
+
export class LoggieCursorPersistenceError extends Error {
|
|
5
|
+
eventId;
|
|
6
|
+
cursor;
|
|
7
|
+
accountId;
|
|
8
|
+
agentProfileId;
|
|
9
|
+
phase;
|
|
10
|
+
constructor(params) {
|
|
11
|
+
const causeMessage = params.cause instanceof Error ? params.cause.message : String(params.cause);
|
|
12
|
+
super(`Loggie cursor persistence failed after dispatching event ${params.event.eventId}: ${causeMessage}`, {
|
|
13
|
+
cause: params.cause,
|
|
14
|
+
});
|
|
15
|
+
this.name = "LoggieCursorPersistenceError";
|
|
16
|
+
this.eventId = params.event.eventId;
|
|
17
|
+
this.cursor = params.event.cursor;
|
|
18
|
+
this.accountId = params.account.accountId;
|
|
19
|
+
this.agentProfileId = params.account.agentProfileId;
|
|
20
|
+
this.phase = params.phase;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function isLoggieCursorPersistenceError(error) {
|
|
24
|
+
return error instanceof LoggieCursorPersistenceError;
|
|
25
|
+
}
|
|
4
26
|
export async function handleLoggieEvent(params) {
|
|
5
27
|
const parsed = parseLoggieEventEnvelope(params.raw);
|
|
6
28
|
if (!parsed.ok) {
|
|
@@ -79,12 +101,23 @@ export async function handleLoggieEvent(params) {
|
|
|
79
101
|
},
|
|
80
102
|
};
|
|
81
103
|
}
|
|
82
|
-
|
|
104
|
+
const advanced = advanceCursor({
|
|
83
105
|
record: cursor,
|
|
84
106
|
event,
|
|
85
107
|
accountId: params.account.accountId,
|
|
86
108
|
agentProfileId: params.account.agentProfileId,
|
|
87
109
|
now: params.now,
|
|
88
|
-
})
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
await params.cursorStore.save(advanced);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw new LoggieCursorPersistenceError({
|
|
116
|
+
event,
|
|
117
|
+
account: params.account,
|
|
118
|
+
phase: "post_dispatch",
|
|
119
|
+
cause: error,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
89
122
|
return { status: "dispatched", event };
|
|
90
123
|
}
|
|
@@ -2,7 +2,14 @@ import { fetchLoggieTranscriptDetail } from "../loggie-client.js";
|
|
|
2
2
|
import { buildLoggieRouteSessionKey, buildLoggieSessionId, meetingConversationId } from "../routing.js";
|
|
3
3
|
import { formatTranscriptReadyPrompt } from "./transcript-format.js";
|
|
4
4
|
export function createOpenClawTranscriptDispatcher(params) {
|
|
5
|
-
|
|
5
|
+
let agentRuntimePromise;
|
|
6
|
+
const getAgentRuntime = () => {
|
|
7
|
+
agentRuntimePromise ??= resolveAgentRuntime(params.runtime).catch((error) => {
|
|
8
|
+
agentRuntimePromise = undefined;
|
|
9
|
+
throw error;
|
|
10
|
+
});
|
|
11
|
+
return agentRuntimePromise;
|
|
12
|
+
};
|
|
6
13
|
return {
|
|
7
14
|
async dispatchTranscriptReady(event) {
|
|
8
15
|
const routeSessionKey = buildLoggieRouteSessionKey({ account: params.account, event });
|
|
@@ -86,6 +93,7 @@ export function createOpenClawTranscriptDispatcher(params) {
|
|
|
86
93
|
},
|
|
87
94
|
},
|
|
88
95
|
runDispatch: async () => {
|
|
96
|
+
const agentRuntime = await getAgentRuntime();
|
|
89
97
|
const workspaceDir = agentRuntime.resolveAgentWorkspaceDir(params.cfg, params.account.agentId);
|
|
90
98
|
await agentRuntime.ensureAgentWorkspace?.({ dir: workspaceDir });
|
|
91
99
|
return agentRuntime.runEmbeddedAgent({
|
|
@@ -122,3 +130,44 @@ export function createOpenClawTranscriptDispatcher(params) {
|
|
|
122
130
|
},
|
|
123
131
|
};
|
|
124
132
|
}
|
|
133
|
+
async function resolveAgentRuntime(runtime) {
|
|
134
|
+
const runtimeAgent = runtime.agent;
|
|
135
|
+
if (hasRequiredAgentHelpers(runtimeAgent)) {
|
|
136
|
+
return runtimeAgent;
|
|
137
|
+
}
|
|
138
|
+
const extensionApi = (await import("openclaw/extension-api"));
|
|
139
|
+
return {
|
|
140
|
+
runEmbeddedAgent: readRunEmbeddedAgent(runtimeAgent, extensionApi),
|
|
141
|
+
resolveAgentWorkspaceDir: readHelper(runtimeAgent, extensionApi, "resolveAgentWorkspaceDir"),
|
|
142
|
+
resolveAgentDir: readHelper(runtimeAgent, extensionApi, "resolveAgentDir"),
|
|
143
|
+
resolveAgentTimeoutMs: readHelper(runtimeAgent, extensionApi, "resolveAgentTimeoutMs"),
|
|
144
|
+
ensureAgentWorkspace: readOptionalHelper(runtimeAgent, extensionApi, "ensureAgentWorkspace"),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function hasRequiredAgentHelpers(value) {
|
|
148
|
+
return (typeof value?.runEmbeddedAgent === "function" &&
|
|
149
|
+
typeof value.resolveAgentWorkspaceDir === "function" &&
|
|
150
|
+
typeof value.resolveAgentDir === "function" &&
|
|
151
|
+
typeof value.resolveAgentTimeoutMs === "function");
|
|
152
|
+
}
|
|
153
|
+
function readRunEmbeddedAgent(runtimeAgent, extensionApi) {
|
|
154
|
+
const helper = runtimeAgent?.runEmbeddedAgent ?? extensionApi.runEmbeddedAgent ?? extensionApi.runEmbeddedPiAgent;
|
|
155
|
+
if (typeof helper !== "function") {
|
|
156
|
+
throw new Error("Loggie transcript dispatch requires OpenClaw agent helper: runEmbeddedAgent");
|
|
157
|
+
}
|
|
158
|
+
return helper;
|
|
159
|
+
}
|
|
160
|
+
function readHelper(runtimeAgent, extensionApi, key) {
|
|
161
|
+
const helper = runtimeAgent?.[key] ?? extensionApi[key];
|
|
162
|
+
if (typeof helper !== "function") {
|
|
163
|
+
throw new Error(`Loggie transcript dispatch requires OpenClaw agent helper: ${key}`);
|
|
164
|
+
}
|
|
165
|
+
return helper;
|
|
166
|
+
}
|
|
167
|
+
function readOptionalHelper(runtimeAgent, extensionApi, key) {
|
|
168
|
+
const helper = runtimeAgent?.[key] ?? extensionApi[key];
|
|
169
|
+
if (helper !== undefined && typeof helper !== "function") {
|
|
170
|
+
throw new Error(`Loggie transcript dispatch requires OpenClaw agent helper: ${key}`);
|
|
171
|
+
}
|
|
172
|
+
return helper;
|
|
173
|
+
}
|
package/dist/src/status.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ export type LoggieRuntimeExtra = {
|
|
|
3
3
|
connected?: boolean;
|
|
4
4
|
authenticated?: boolean;
|
|
5
5
|
caughtUp?: boolean;
|
|
6
|
+
cursorStoreMode?: "trusted" | "file" | null;
|
|
7
|
+
cursorPersistenceFailed?: boolean;
|
|
8
|
+
lastCursorPersistenceAt?: number | null;
|
|
9
|
+
lastCursorPersistenceError?: string | null;
|
|
10
|
+
lastCursorPersistenceEventId?: string | null;
|
|
11
|
+
lastCursorPersistenceCursor?: string | null;
|
|
6
12
|
lastEventId?: string | null;
|
|
7
13
|
lastCursor?: string | null;
|
|
8
14
|
lastSequence?: number | null;
|
|
@@ -33,6 +39,12 @@ export declare function buildLoggieAccountStatusSnapshot(params: {
|
|
|
33
39
|
connected: {};
|
|
34
40
|
authenticated: {};
|
|
35
41
|
caughtUp: {};
|
|
42
|
+
cursorStoreMode: {} | null;
|
|
43
|
+
cursorPersistenceFailed: {};
|
|
44
|
+
lastCursorPersistenceAt: {} | null;
|
|
45
|
+
lastCursorPersistenceError: {} | null;
|
|
46
|
+
lastCursorPersistenceEventId: {} | null;
|
|
47
|
+
lastCursorPersistenceCursor: {} | null;
|
|
36
48
|
lastEventId: {} | null;
|
|
37
49
|
lastCursor: {} | null;
|
|
38
50
|
lastSequence: {} | null;
|
package/dist/src/status.js
CHANGED
|
@@ -3,6 +3,12 @@ export const defaultLoggieRuntimeState = createDefaultChannelRuntimeState("defau
|
|
|
3
3
|
connected: false,
|
|
4
4
|
authenticated: false,
|
|
5
5
|
caughtUp: false,
|
|
6
|
+
cursorStoreMode: null,
|
|
7
|
+
cursorPersistenceFailed: false,
|
|
8
|
+
lastCursorPersistenceAt: null,
|
|
9
|
+
lastCursorPersistenceError: null,
|
|
10
|
+
lastCursorPersistenceEventId: null,
|
|
11
|
+
lastCursorPersistenceCursor: null,
|
|
6
12
|
lastEventId: null,
|
|
7
13
|
lastCursor: null,
|
|
8
14
|
lastSequence: null,
|
|
@@ -32,6 +38,12 @@ export function buildLoggieAccountStatusSnapshot(params) {
|
|
|
32
38
|
connected: params.runtime?.connected ?? false,
|
|
33
39
|
authenticated: params.runtime?.authenticated ?? false,
|
|
34
40
|
caughtUp: params.runtime?.caughtUp ?? false,
|
|
41
|
+
cursorStoreMode: params.runtime?.cursorStoreMode ?? null,
|
|
42
|
+
cursorPersistenceFailed: params.runtime?.cursorPersistenceFailed ?? false,
|
|
43
|
+
lastCursorPersistenceAt: params.runtime?.lastCursorPersistenceAt ?? null,
|
|
44
|
+
lastCursorPersistenceError: params.runtime?.lastCursorPersistenceError ?? null,
|
|
45
|
+
lastCursorPersistenceEventId: params.runtime?.lastCursorPersistenceEventId ?? null,
|
|
46
|
+
lastCursorPersistenceCursor: params.runtime?.lastCursorPersistenceCursor ?? null,
|
|
35
47
|
lastEventId: params.runtime?.lastEventId ?? null,
|
|
36
48
|
lastCursor: params.runtime?.lastCursor ?? null,
|
|
37
49
|
lastSequence: params.runtime?.lastSequence ?? null,
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/cursor-store.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
1
4
|
import type { LoggieEventEnvelope } from "./event-types.js";
|
|
2
5
|
|
|
3
6
|
export type LoggieCursorRecord = {
|
|
@@ -39,13 +42,21 @@ export type LoggieCursorDecision =
|
|
|
39
42
|
| { action: "skip"; reason: "duplicate_event" | "cursor_regression" };
|
|
40
43
|
|
|
41
44
|
export type LoggieCursorStore = {
|
|
45
|
+
readonly mode?: LoggieCursorStoreMode;
|
|
42
46
|
load(accountId: string, agentProfileId: string): Promise<LoggieCursorRecord | undefined>;
|
|
43
47
|
save(record: LoggieCursorRecord): Promise<void>;
|
|
44
48
|
};
|
|
45
49
|
|
|
50
|
+
export type LoggieCursorStoreMode = "trusted" | "file";
|
|
51
|
+
|
|
52
|
+
export type RuntimeCursorStoreOptions = {
|
|
53
|
+
warn?: (message: string) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
46
56
|
const MAX_SEEN_EVENT_IDS = 500;
|
|
47
57
|
const MAX_FAILED_EVENTS = 50;
|
|
48
58
|
const MAX_DEAD_LETTERED_EVENTS = 100;
|
|
59
|
+
const warnedFileCursorStorePaths = new Set<string>();
|
|
49
60
|
|
|
50
61
|
export function createEmptyCursorRecord(params: {
|
|
51
62
|
accountId: string;
|
|
@@ -193,27 +204,111 @@ export function createMemoryCursorStore(initial?: LoggieCursorRecord[]): LoggieC
|
|
|
193
204
|
};
|
|
194
205
|
}
|
|
195
206
|
|
|
196
|
-
export function
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
export function createFileCursorStore(filePath = defaultCursorStorePath()): LoggieCursorStore {
|
|
208
|
+
let pending: Promise<void> = Promise.resolve();
|
|
209
|
+
|
|
210
|
+
async function withFileLock<T>(operation: () => Promise<T>): Promise<T> {
|
|
211
|
+
const result = pending.then(operation, operation);
|
|
212
|
+
pending = result.then(
|
|
213
|
+
() => undefined,
|
|
214
|
+
() => undefined,
|
|
215
|
+
);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
mode: "file",
|
|
221
|
+
load(accountId, agentProfileId) {
|
|
222
|
+
return withFileLock(async () => {
|
|
223
|
+
const records = await readCursorFile(filePath);
|
|
224
|
+
return records[cursorKey(accountId, agentProfileId)];
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
save(record) {
|
|
228
|
+
return withFileLock(async () => {
|
|
229
|
+
const records = await readCursorFile(filePath);
|
|
230
|
+
records[cursorKey(record.accountId, record.agentProfileId)] = record;
|
|
231
|
+
await writeCursorFile(filePath, records);
|
|
232
|
+
});
|
|
233
|
+
},
|
|
202
234
|
};
|
|
203
|
-
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function createRuntimeCursorStore(
|
|
238
|
+
runtime: {
|
|
239
|
+
state?: {
|
|
240
|
+
resolveStateDir?: () => string;
|
|
241
|
+
openKeyedStore?: <T>(opts: { namespace: string; maxEntries: number }) => {
|
|
242
|
+
lookup(key: string): Promise<T | undefined>;
|
|
243
|
+
register(key: string, value: T): Promise<void>;
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
options: RuntimeCursorStoreOptions = {},
|
|
248
|
+
): LoggieCursorStore {
|
|
204
249
|
const store = runtime.state?.openKeyedStore?.<LoggieCursorRecord>({
|
|
205
250
|
namespace: "loggie-cursors",
|
|
206
251
|
maxEntries: 1000,
|
|
207
252
|
});
|
|
208
253
|
if (!store) {
|
|
209
|
-
|
|
254
|
+
const stateDir = runtime.state?.resolveStateDir?.();
|
|
255
|
+
const filePath = stateDir ? join(stateDir, "loggie-cursors.json") : defaultCursorStorePath();
|
|
256
|
+
warnFileCursorStoreFallbackOnce(filePath, options.warn);
|
|
257
|
+
return createFileCursorStore(filePath);
|
|
210
258
|
}
|
|
211
259
|
return {
|
|
260
|
+
mode: "trusted",
|
|
212
261
|
load: (accountId, agentProfileId) => store.lookup(cursorKey(accountId, agentProfileId)),
|
|
213
262
|
save: (record) => store.register(cursorKey(record.accountId, record.agentProfileId), record),
|
|
214
263
|
};
|
|
215
264
|
}
|
|
216
265
|
|
|
266
|
+
function defaultCursorStorePath(): string {
|
|
267
|
+
return join(homedir(), ".openclaw", "state", "loggie-cursors.json");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function warnFileCursorStoreFallbackOnce(filePath: string, warn: RuntimeCursorStoreOptions["warn"]): void {
|
|
271
|
+
if (!warn || warnedFileCursorStorePaths.has(filePath)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
warnedFileCursorStorePaths.add(filePath);
|
|
275
|
+
warn(
|
|
276
|
+
`Loggie trusted runtime cursor store is unavailable; using file-backed cursor store at ${filePath}. Cursor durability now depends on filesystem access to that path.`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function readCursorFile(filePath: string): Promise<Record<string, LoggieCursorRecord>> {
|
|
281
|
+
try {
|
|
282
|
+
const contents = await fs.readFile(filePath, "utf8");
|
|
283
|
+
const parsed = JSON.parse(contents) as unknown;
|
|
284
|
+
if (isCursorFile(parsed)) {
|
|
285
|
+
return parsed;
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`Loggie cursor store file has invalid JSON shape: ${filePath}`);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
290
|
+
return {};
|
|
291
|
+
}
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function writeCursorFile(filePath: string, records: Record<string, LoggieCursorRecord>): Promise<void> {
|
|
297
|
+
const directory = dirname(filePath);
|
|
298
|
+
await fs.mkdir(directory, { recursive: true });
|
|
299
|
+
const tempPath = join(directory, `.loggie-cursors.${process.pid}.${Date.now()}.tmp`);
|
|
300
|
+
await fs.writeFile(tempPath, `${JSON.stringify(records, null, 2)}\n`, "utf8");
|
|
301
|
+
await fs.rename(tempPath, filePath);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isCursorFile(value: unknown): value is Record<string, LoggieCursorRecord> {
|
|
305
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
309
|
+
return error instanceof Error && "code" in error;
|
|
310
|
+
}
|
|
311
|
+
|
|
217
312
|
function cursorKey(accountId: string, agentProfileId: string): string {
|
|
218
313
|
return `${accountId}:${agentProfileId}`;
|
|
219
314
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/channel-core";
|
|
2
2
|
import type { ResolvedLoggieAccount } from "../account.js";
|
|
3
|
-
import { createRuntimeCursorStore, type LoggieCursorStore } from "../cursor-store.js";
|
|
3
|
+
import { createRuntimeCursorStore, type LoggieCursorStore, type LoggieCursorStoreMode } from "../cursor-store.js";
|
|
4
4
|
import {
|
|
5
5
|
buildLoggieAuthHeaders,
|
|
6
6
|
buildLoggiePingMessage,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type LoggieSocketFactory,
|
|
12
12
|
} from "../loggie-client.js";
|
|
13
13
|
import { createOpenClawTranscriptDispatcher, type TranscriptDispatcher } from "./transcript-dispatch.js";
|
|
14
|
-
import { handleLoggieEvent } from "./event-handler.js";
|
|
14
|
+
import { handleLoggieEvent, isLoggieCursorPersistenceError } from "./event-handler.js";
|
|
15
15
|
|
|
16
16
|
type ChannelRuntime = PluginRuntime["channel"];
|
|
17
17
|
|
|
@@ -45,7 +45,7 @@ export async function monitorLoggieAccount(params: MonitorLoggieAccountParams):
|
|
|
45
45
|
throw new Error("Loggie channelRuntime is required for transcript dispatch");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const cursorStore = params.cursorStore ?? createRuntimeCursorStore(params.runtime);
|
|
48
|
+
const cursorStore = params.cursorStore ?? createRuntimeCursorStore(params.runtime, { warn: params.log?.warn });
|
|
49
49
|
const dispatcher =
|
|
50
50
|
params.dispatcher ??
|
|
51
51
|
createOpenClawTranscriptDispatcher({
|
|
@@ -71,6 +71,26 @@ export async function monitorLoggieAccount(params: MonitorLoggieAccountParams):
|
|
|
71
71
|
if (params.abortSignal?.aborted) {
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
+
if (isLoggieCursorPersistenceError(error)) {
|
|
75
|
+
const failedAt = Date.now();
|
|
76
|
+
params.setStatus?.({
|
|
77
|
+
...(params.getStatus?.() ?? { accountId: params.account.accountId }),
|
|
78
|
+
running: false,
|
|
79
|
+
connected: false,
|
|
80
|
+
authenticated: false,
|
|
81
|
+
...cursorStoreStatus(cursorStore.mode),
|
|
82
|
+
cursorPersistenceFailed: true,
|
|
83
|
+
lastCursorPersistenceAt: failedAt,
|
|
84
|
+
lastCursorPersistenceError: error.message,
|
|
85
|
+
lastCursorPersistenceEventId: error.eventId,
|
|
86
|
+
lastCursorPersistenceCursor: error.cursor ?? null,
|
|
87
|
+
lastError: error.message,
|
|
88
|
+
});
|
|
89
|
+
params.log?.warn?.(
|
|
90
|
+
`[${params.account.accountId}] Loggie cursor persistence failed; stopping monitor to avoid duplicate transcript dispatch: ${error.message}`,
|
|
91
|
+
);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
74
94
|
attempts += 1;
|
|
75
95
|
const delayMs = Math.min(
|
|
76
96
|
params.account.reconnect.maxMs,
|
|
@@ -82,6 +102,8 @@ export async function monitorLoggieAccount(params: MonitorLoggieAccountParams):
|
|
|
82
102
|
connected: false,
|
|
83
103
|
authenticated: false,
|
|
84
104
|
reconnectAttempts: attempts,
|
|
105
|
+
...cursorStoreStatus(cursorStore.mode),
|
|
106
|
+
...cursorPersistenceOkStatus(),
|
|
85
107
|
lastError: error instanceof Error ? error.message : String(error),
|
|
86
108
|
});
|
|
87
109
|
params.log?.warn?.(
|
|
@@ -107,6 +129,8 @@ export async function runLoggieSocketOnce(params: MonitorLoggieAccountParams & {
|
|
|
107
129
|
running: true,
|
|
108
130
|
connected: false,
|
|
109
131
|
authenticated: false,
|
|
132
|
+
...cursorStoreStatus(params.cursorStore.mode),
|
|
133
|
+
...cursorPersistenceOkStatus(),
|
|
110
134
|
lastStartAt: Date.now(),
|
|
111
135
|
});
|
|
112
136
|
|
|
@@ -187,6 +211,8 @@ export async function runLoggieSocketOnce(params: MonitorLoggieAccountParams & {
|
|
|
187
211
|
connected: true,
|
|
188
212
|
authenticated: true,
|
|
189
213
|
reconnectAttempts: 0,
|
|
214
|
+
...cursorStoreStatus(params.cursorStore.mode),
|
|
215
|
+
...cursorPersistenceOkStatus(),
|
|
190
216
|
lastConnectedAt: connectedAt,
|
|
191
217
|
lastError: null,
|
|
192
218
|
});
|
|
@@ -253,6 +279,8 @@ export async function runLoggieSocketOnce(params: MonitorLoggieAccountParams & {
|
|
|
253
279
|
connected: true,
|
|
254
280
|
authenticated: true,
|
|
255
281
|
caughtUp: true,
|
|
282
|
+
...cursorStoreStatus(params.cursorStore.mode),
|
|
283
|
+
...cursorPersistenceOkStatus(),
|
|
256
284
|
lastEventAt: eventAt,
|
|
257
285
|
...(result.status === "dispatched" ? { lastDispatchAt: eventAt } : {}),
|
|
258
286
|
...(deadLettered && result.event
|
|
@@ -302,6 +330,20 @@ export async function runLoggieSocketOnce(params: MonitorLoggieAccountParams & {
|
|
|
302
330
|
});
|
|
303
331
|
}
|
|
304
332
|
|
|
333
|
+
function cursorStoreStatus(mode: LoggieCursorStoreMode | undefined): { cursorStoreMode?: LoggieCursorStoreMode } {
|
|
334
|
+
return mode ? { cursorStoreMode: mode } : {};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function cursorPersistenceOkStatus() {
|
|
338
|
+
return {
|
|
339
|
+
cursorPersistenceFailed: false,
|
|
340
|
+
lastCursorPersistenceAt: null,
|
|
341
|
+
lastCursorPersistenceError: null,
|
|
342
|
+
lastCursorPersistenceEventId: null,
|
|
343
|
+
lastCursorPersistenceCursor: null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
305
347
|
type ControlMessageDecision =
|
|
306
348
|
| { type: "none" }
|
|
307
349
|
| { type: "ignore" }
|
|
@@ -28,6 +28,36 @@ export type LoggieEventHandleResult =
|
|
|
28
28
|
|
|
29
29
|
const MAX_EVENT_DISPATCH_ATTEMPTS = 3;
|
|
30
30
|
|
|
31
|
+
export class LoggieCursorPersistenceError extends Error {
|
|
32
|
+
readonly eventId: string;
|
|
33
|
+
readonly cursor?: string;
|
|
34
|
+
readonly accountId: string;
|
|
35
|
+
readonly agentProfileId: string;
|
|
36
|
+
readonly phase: "post_dispatch";
|
|
37
|
+
|
|
38
|
+
constructor(params: {
|
|
39
|
+
event: LoggieEventEnvelope;
|
|
40
|
+
account: ResolvedLoggieAccount;
|
|
41
|
+
phase: "post_dispatch";
|
|
42
|
+
cause: unknown;
|
|
43
|
+
}) {
|
|
44
|
+
const causeMessage = params.cause instanceof Error ? params.cause.message : String(params.cause);
|
|
45
|
+
super(`Loggie cursor persistence failed after dispatching event ${params.event.eventId}: ${causeMessage}`, {
|
|
46
|
+
cause: params.cause,
|
|
47
|
+
});
|
|
48
|
+
this.name = "LoggieCursorPersistenceError";
|
|
49
|
+
this.eventId = params.event.eventId;
|
|
50
|
+
this.cursor = params.event.cursor;
|
|
51
|
+
this.accountId = params.account.accountId;
|
|
52
|
+
this.agentProfileId = params.account.agentProfileId;
|
|
53
|
+
this.phase = params.phase;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isLoggieCursorPersistenceError(error: unknown): error is LoggieCursorPersistenceError {
|
|
58
|
+
return error instanceof LoggieCursorPersistenceError;
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
export async function handleLoggieEvent(params: {
|
|
32
62
|
raw: unknown;
|
|
33
63
|
account: ResolvedLoggieAccount;
|
|
@@ -120,14 +150,22 @@ export async function handleLoggieEvent(params: {
|
|
|
120
150
|
},
|
|
121
151
|
};
|
|
122
152
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
const advanced = advanceCursor({
|
|
154
|
+
record: cursor,
|
|
155
|
+
event,
|
|
156
|
+
accountId: params.account.accountId,
|
|
157
|
+
agentProfileId: params.account.agentProfileId,
|
|
158
|
+
now: params.now,
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
await params.cursorStore.save(advanced);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new LoggieCursorPersistenceError({
|
|
126
164
|
event,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
})
|
|
131
|
-
|
|
165
|
+
account: params.account,
|
|
166
|
+
phase: "post_dispatch",
|
|
167
|
+
cause: error,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
132
170
|
return { status: "dispatched", event };
|
|
133
171
|
}
|
|
@@ -10,6 +10,26 @@ export type TranscriptDispatcher = {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
type ChannelRuntime = PluginRuntime["channel"];
|
|
13
|
+
type AgentRuntimeHelpers = {
|
|
14
|
+
runEmbeddedAgent(input: Record<string, unknown>): Promise<unknown>;
|
|
15
|
+
resolveAgentWorkspaceDir(
|
|
16
|
+
cfg: OpenClawConfig,
|
|
17
|
+
agentId: string,
|
|
18
|
+
env?: NodeJS.ProcessEnv,
|
|
19
|
+
): string;
|
|
20
|
+
resolveAgentDir(cfg: OpenClawConfig, agentId: string, env?: NodeJS.ProcessEnv): string;
|
|
21
|
+
resolveAgentTimeoutMs(opts: {
|
|
22
|
+
cfg?: OpenClawConfig;
|
|
23
|
+
overrideMs?: number | null;
|
|
24
|
+
overrideSeconds?: number | null;
|
|
25
|
+
minMs?: number;
|
|
26
|
+
}): number;
|
|
27
|
+
ensureAgentWorkspace?: (params: { dir: string }) => Promise<void> | void;
|
|
28
|
+
};
|
|
29
|
+
type ExtensionApi = Partial<AgentRuntimeHelpers>;
|
|
30
|
+
type ExtensionApiWithAliases = ExtensionApi & {
|
|
31
|
+
runEmbeddedPiAgent?: AgentRuntimeHelpers["runEmbeddedAgent"];
|
|
32
|
+
};
|
|
13
33
|
|
|
14
34
|
export function createOpenClawTranscriptDispatcher(params: {
|
|
15
35
|
account: ResolvedLoggieAccount;
|
|
@@ -19,7 +39,15 @@ export function createOpenClawTranscriptDispatcher(params: {
|
|
|
19
39
|
abortSignal?: AbortSignal;
|
|
20
40
|
fetchImpl?: typeof fetch;
|
|
21
41
|
}): TranscriptDispatcher {
|
|
22
|
-
|
|
42
|
+
let agentRuntimePromise: Promise<AgentRuntimeHelpers> | undefined;
|
|
43
|
+
const getAgentRuntime = () => {
|
|
44
|
+
agentRuntimePromise ??= resolveAgentRuntime(params.runtime).catch((error: unknown) => {
|
|
45
|
+
agentRuntimePromise = undefined;
|
|
46
|
+
throw error;
|
|
47
|
+
});
|
|
48
|
+
return agentRuntimePromise;
|
|
49
|
+
};
|
|
50
|
+
|
|
23
51
|
return {
|
|
24
52
|
async dispatchTranscriptReady(event) {
|
|
25
53
|
const routeSessionKey = buildLoggieRouteSessionKey({ account: params.account, event });
|
|
@@ -104,6 +132,7 @@ export function createOpenClawTranscriptDispatcher(params: {
|
|
|
104
132
|
},
|
|
105
133
|
},
|
|
106
134
|
runDispatch: async () => {
|
|
135
|
+
const agentRuntime = await getAgentRuntime();
|
|
107
136
|
const workspaceDir = agentRuntime.resolveAgentWorkspaceDir(
|
|
108
137
|
params.cfg,
|
|
109
138
|
params.account.agentId,
|
|
@@ -143,3 +172,63 @@ export function createOpenClawTranscriptDispatcher(params: {
|
|
|
143
172
|
},
|
|
144
173
|
};
|
|
145
174
|
}
|
|
175
|
+
|
|
176
|
+
async function resolveAgentRuntime(runtime: PluginRuntime): Promise<AgentRuntimeHelpers> {
|
|
177
|
+
const runtimeAgent = (runtime as { agent?: Partial<AgentRuntimeHelpers> }).agent;
|
|
178
|
+
if (hasRequiredAgentHelpers(runtimeAgent)) {
|
|
179
|
+
return runtimeAgent;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const extensionApi = (await import("openclaw/extension-api")) as ExtensionApiWithAliases;
|
|
183
|
+
return {
|
|
184
|
+
runEmbeddedAgent: readRunEmbeddedAgent(runtimeAgent, extensionApi),
|
|
185
|
+
resolveAgentWorkspaceDir: readHelper(runtimeAgent, extensionApi, "resolveAgentWorkspaceDir"),
|
|
186
|
+
resolveAgentDir: readHelper(runtimeAgent, extensionApi, "resolveAgentDir"),
|
|
187
|
+
resolveAgentTimeoutMs: readHelper(runtimeAgent, extensionApi, "resolveAgentTimeoutMs"),
|
|
188
|
+
ensureAgentWorkspace: readOptionalHelper(runtimeAgent, extensionApi, "ensureAgentWorkspace"),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function hasRequiredAgentHelpers(value: Partial<AgentRuntimeHelpers> | undefined): value is AgentRuntimeHelpers {
|
|
193
|
+
return (
|
|
194
|
+
typeof value?.runEmbeddedAgent === "function" &&
|
|
195
|
+
typeof value.resolveAgentWorkspaceDir === "function" &&
|
|
196
|
+
typeof value.resolveAgentDir === "function" &&
|
|
197
|
+
typeof value.resolveAgentTimeoutMs === "function"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readRunEmbeddedAgent(
|
|
202
|
+
runtimeAgent: Partial<AgentRuntimeHelpers> | undefined,
|
|
203
|
+
extensionApi: ExtensionApiWithAliases,
|
|
204
|
+
): AgentRuntimeHelpers["runEmbeddedAgent"] {
|
|
205
|
+
const helper = runtimeAgent?.runEmbeddedAgent ?? extensionApi.runEmbeddedAgent ?? extensionApi.runEmbeddedPiAgent;
|
|
206
|
+
if (typeof helper !== "function") {
|
|
207
|
+
throw new Error("Loggie transcript dispatch requires OpenClaw agent helper: runEmbeddedAgent");
|
|
208
|
+
}
|
|
209
|
+
return helper;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readHelper<K extends keyof AgentRuntimeHelpers>(
|
|
213
|
+
runtimeAgent: Partial<AgentRuntimeHelpers> | undefined,
|
|
214
|
+
extensionApi: ExtensionApi,
|
|
215
|
+
key: K,
|
|
216
|
+
): AgentRuntimeHelpers[K] {
|
|
217
|
+
const helper = runtimeAgent?.[key] ?? extensionApi[key];
|
|
218
|
+
if (typeof helper !== "function") {
|
|
219
|
+
throw new Error(`Loggie transcript dispatch requires OpenClaw agent helper: ${key}`);
|
|
220
|
+
}
|
|
221
|
+
return helper as AgentRuntimeHelpers[K];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readOptionalHelper<K extends keyof AgentRuntimeHelpers>(
|
|
225
|
+
runtimeAgent: Partial<AgentRuntimeHelpers> | undefined,
|
|
226
|
+
extensionApi: ExtensionApi,
|
|
227
|
+
key: K,
|
|
228
|
+
): AgentRuntimeHelpers[K] | undefined {
|
|
229
|
+
const helper = runtimeAgent?.[key] ?? extensionApi[key];
|
|
230
|
+
if (helper !== undefined && typeof helper !== "function") {
|
|
231
|
+
throw new Error(`Loggie transcript dispatch requires OpenClaw agent helper: ${key}`);
|
|
232
|
+
}
|
|
233
|
+
return helper as AgentRuntimeHelpers[K] | undefined;
|
|
234
|
+
}
|
package/src/status.ts
CHANGED
|
@@ -5,6 +5,12 @@ export type LoggieRuntimeExtra = {
|
|
|
5
5
|
connected?: boolean;
|
|
6
6
|
authenticated?: boolean;
|
|
7
7
|
caughtUp?: boolean;
|
|
8
|
+
cursorStoreMode?: "trusted" | "file" | null;
|
|
9
|
+
cursorPersistenceFailed?: boolean;
|
|
10
|
+
lastCursorPersistenceAt?: number | null;
|
|
11
|
+
lastCursorPersistenceError?: string | null;
|
|
12
|
+
lastCursorPersistenceEventId?: string | null;
|
|
13
|
+
lastCursorPersistenceCursor?: string | null;
|
|
8
14
|
lastEventId?: string | null;
|
|
9
15
|
lastCursor?: string | null;
|
|
10
16
|
lastSequence?: number | null;
|
|
@@ -22,6 +28,12 @@ export const defaultLoggieRuntimeState = createDefaultChannelRuntimeState<Loggie
|
|
|
22
28
|
connected: false,
|
|
23
29
|
authenticated: false,
|
|
24
30
|
caughtUp: false,
|
|
31
|
+
cursorStoreMode: null,
|
|
32
|
+
cursorPersistenceFailed: false,
|
|
33
|
+
lastCursorPersistenceAt: null,
|
|
34
|
+
lastCursorPersistenceError: null,
|
|
35
|
+
lastCursorPersistenceEventId: null,
|
|
36
|
+
lastCursorPersistenceCursor: null,
|
|
25
37
|
lastEventId: null,
|
|
26
38
|
lastCursor: null,
|
|
27
39
|
lastSequence: null,
|
|
@@ -58,6 +70,12 @@ export function buildLoggieAccountStatusSnapshot(params: {
|
|
|
58
70
|
connected: params.runtime?.connected ?? false,
|
|
59
71
|
authenticated: params.runtime?.authenticated ?? false,
|
|
60
72
|
caughtUp: params.runtime?.caughtUp ?? false,
|
|
73
|
+
cursorStoreMode: params.runtime?.cursorStoreMode ?? null,
|
|
74
|
+
cursorPersistenceFailed: params.runtime?.cursorPersistenceFailed ?? false,
|
|
75
|
+
lastCursorPersistenceAt: params.runtime?.lastCursorPersistenceAt ?? null,
|
|
76
|
+
lastCursorPersistenceError: params.runtime?.lastCursorPersistenceError ?? null,
|
|
77
|
+
lastCursorPersistenceEventId: params.runtime?.lastCursorPersistenceEventId ?? null,
|
|
78
|
+
lastCursorPersistenceCursor: params.runtime?.lastCursorPersistenceCursor ?? null,
|
|
61
79
|
lastEventId: params.runtime?.lastEventId ?? null,
|
|
62
80
|
lastCursor: params.runtime?.lastCursor ?? null,
|
|
63
81
|
lastSequence: params.runtime?.lastSequence ?? null,
|