forge-openclaw-plugin 0.2.115 → 0.2.117
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/server/server/src/managers/platform/background-job-manager.js +1 -0
- package/dist/server/server/src/managers/platform/openai-responses-provider.js +63 -4
- package/dist/server/server/src/repositories/wiki-memory.js +136 -1
- package/dist/server/server/src/services/companion-iroh.js +6 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/companion-iroh/darwin-arm64/forge-companion-iroh +0 -0
- package/dist/companion-iroh/darwin-x64/forge-companion-iroh +0 -0
- package/dist/companion-iroh/linux-x64/forge-companion-iroh +0 -0
- package/dist/companion-iroh/win32-x64/forge-companion-iroh.exe +0 -0
|
@@ -20,8 +20,12 @@ const MODEL_CONTEXT_WINDOWS = {
|
|
|
20
20
|
};
|
|
21
21
|
const DEFAULT_CONTEXT_WINDOW = 400_000;
|
|
22
22
|
const RESERVED_RESPONSE_TOKENS = 140_000;
|
|
23
|
+
const CODEX_WIKI_COMPILE_CONTEXT_WINDOW = 120_000;
|
|
24
|
+
const CODEX_WIKI_COMPILE_RESERVED_RESPONSE_TOKENS = 60_000;
|
|
23
25
|
const APPROX_CHARS_PER_TOKEN = 4;
|
|
24
26
|
const REQUEST_TIMEOUT_MS = 90_000;
|
|
27
|
+
const CODEX_FOREGROUND_COMPILE_TIMEOUT_MS = 10 * 60_000;
|
|
28
|
+
const CODEX_STREAM_READ_TIMEOUT_MS = 10 * 60_000;
|
|
25
29
|
const BACKGROUND_POLL_INTERVAL_MS = 2_000;
|
|
26
30
|
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
27
31
|
const CODEX_JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
@@ -261,9 +265,52 @@ function parseCodexEventStreamPayload(streamText) {
|
|
|
261
265
|
}
|
|
262
266
|
async function readProviderPayload(response, profile) {
|
|
263
267
|
return isCodexProfile(profile)
|
|
264
|
-
? parseCodexEventStreamPayload(await response
|
|
268
|
+
? parseCodexEventStreamPayload(await readResponseTextWithTimeout(response, CODEX_STREAM_READ_TIMEOUT_MS))
|
|
265
269
|
: readJsonPayload(response);
|
|
266
270
|
}
|
|
271
|
+
async function readResponseTextWithTimeout(response, timeoutMs) {
|
|
272
|
+
if (!response.body) {
|
|
273
|
+
return response.text();
|
|
274
|
+
}
|
|
275
|
+
const reader = response.body.getReader();
|
|
276
|
+
const decoder = new TextDecoder();
|
|
277
|
+
const chunks = [];
|
|
278
|
+
const deadline = Date.now() + timeoutMs;
|
|
279
|
+
try {
|
|
280
|
+
while (true) {
|
|
281
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
282
|
+
const result = await readStreamChunkWithTimeout(reader, remainingMs, timeoutMs);
|
|
283
|
+
if (result.done) {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
chunks.push(decoder.decode(result.value, { stream: true }));
|
|
287
|
+
}
|
|
288
|
+
chunks.push(decoder.decode());
|
|
289
|
+
return chunks.join("");
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
await reader.cancel().catch(() => undefined);
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
reader.releaseLock();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function readStreamChunkWithTimeout(reader, remainingMs, totalTimeoutMs) {
|
|
300
|
+
let timeout = null;
|
|
301
|
+
return Promise.race([
|
|
302
|
+
reader.read(),
|
|
303
|
+
new Promise((_, reject) => {
|
|
304
|
+
timeout = setTimeout(() => {
|
|
305
|
+
reject(new Error(`Codex stream read timed out after ${Math.round(totalTimeoutMs / 1000)}s.`));
|
|
306
|
+
}, remainingMs);
|
|
307
|
+
})
|
|
308
|
+
]).finally(() => {
|
|
309
|
+
if (timeout) {
|
|
310
|
+
clearTimeout(timeout);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
267
314
|
function readReasoningEffort(profile) {
|
|
268
315
|
return typeof profile.metadata.reasoningEffort === "string"
|
|
269
316
|
? profile.metadata.reasoningEffort
|
|
@@ -358,8 +405,14 @@ function estimateTokens(text) {
|
|
|
358
405
|
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
359
406
|
}
|
|
360
407
|
function computeSourceExcerpt(profile, sourceText) {
|
|
361
|
-
const
|
|
362
|
-
const
|
|
408
|
+
const configuredContextWindow = MODEL_CONTEXT_WINDOWS[profile.model] ?? DEFAULT_CONTEXT_WINDOW;
|
|
409
|
+
const contextWindow = isCodexProfile(profile)
|
|
410
|
+
? Math.min(configuredContextWindow, CODEX_WIKI_COMPILE_CONTEXT_WINDOW)
|
|
411
|
+
: configuredContextWindow;
|
|
412
|
+
const reservedResponseTokens = isCodexProfile(profile)
|
|
413
|
+
? CODEX_WIKI_COMPILE_RESERVED_RESPONSE_TOKENS
|
|
414
|
+
: RESERVED_RESPONSE_TOKENS;
|
|
415
|
+
const inputBudget = Math.max(16_000, contextWindow - reservedResponseTokens);
|
|
363
416
|
const estimatedTokens = estimateTokens(sourceText);
|
|
364
417
|
if (estimatedTokens <= inputBudget) {
|
|
365
418
|
return {
|
|
@@ -617,6 +670,10 @@ export class OpenAiResponsesProvider {
|
|
|
617
670
|
"- For chats and transcripts, extract the durable parts: people, relationships, ongoing projects, commitments, habits, values, decisions, questions, sources, and evidence.",
|
|
618
671
|
"- Merge repetitive back-and-forth into concise summaries.",
|
|
619
672
|
"- Use short quotes only when the exact phrase matters.",
|
|
673
|
+
"Sensitive information rules:",
|
|
674
|
+
"- Never store or reproduce secrets, passwords, passphrases, recovery codes, API keys, access tokens, refresh tokens, session cookies, private keys, seed phrases, one-time codes, full payment card numbers, or equivalent credentials.",
|
|
675
|
+
"- If the source contains a secret or credential, replace the value with [REDACTED SECRET] and keep only the minimum non-secret context needed to explain why it appeared.",
|
|
676
|
+
"- Do not create wiki pages, notes, entity proposals, aliases, tags, titles, or page update suggestions that preserve secret values.",
|
|
620
677
|
"How to split pages:",
|
|
621
678
|
"- Keep markdown as the overview page for this source.",
|
|
622
679
|
"- If one topic deserves its own page, put it in articleCandidates with title, slug, summary, rationale, markdown, tags, aliases, and parentSlug.",
|
|
@@ -820,7 +877,9 @@ export class OpenAiResponsesProvider {
|
|
|
820
877
|
}
|
|
821
878
|
})
|
|
822
879
|
}),
|
|
823
|
-
signal: AbortSignal.timeout(
|
|
880
|
+
signal: AbortSignal.timeout(isCodexProfile(profile)
|
|
881
|
+
? CODEX_FOREGROUND_COMPILE_TIMEOUT_MS
|
|
882
|
+
: REQUEST_TIMEOUT_MS)
|
|
824
883
|
});
|
|
825
884
|
}
|
|
826
885
|
catch (error) {
|
|
@@ -10,6 +10,8 @@ import { createNoteLinkSchema, crudEntityTypeSchema, noteKindSchema, noteSchema
|
|
|
10
10
|
import { deleteEncryptedSecret, readEncryptedSecret, storeEncryptedSecret } from "./calendar.js";
|
|
11
11
|
import { isEntityDeleted } from "./deleted-entities.js";
|
|
12
12
|
import { recordDiagnosticLog } from "./diagnostic-logs.js";
|
|
13
|
+
const MAX_WIKI_INGEST_TEXT_CHUNK_CHARS = 220_000;
|
|
14
|
+
const WIKI_INGEST_TEXT_CHUNK_OVERLAP_CHARS = 2_000;
|
|
13
15
|
const wikiSpaceSchema = z.object({
|
|
14
16
|
id: z.string(),
|
|
15
17
|
slug: z.string(),
|
|
@@ -2017,6 +2019,115 @@ export async function reindexWikiEmbeddings(input, secrets) {
|
|
|
2017
2019
|
chunkCount
|
|
2018
2020
|
};
|
|
2019
2021
|
}
|
|
2022
|
+
function isChunkableWikiIngestTextAsset(asset) {
|
|
2023
|
+
const mimeType = asset.mime_type.toLowerCase();
|
|
2024
|
+
const fileName = asset.file_name.toLowerCase();
|
|
2025
|
+
if (!existsSync(asset.file_path)) {
|
|
2026
|
+
return false;
|
|
2027
|
+
}
|
|
2028
|
+
if (asset.size_bytes <= MAX_WIKI_INGEST_TEXT_CHUNK_CHARS) {
|
|
2029
|
+
return false;
|
|
2030
|
+
}
|
|
2031
|
+
const metadata = parseJsonRecord(asset.metadata_json);
|
|
2032
|
+
if (metadata?.chunkParentAssetId || metadata?.textChunked) {
|
|
2033
|
+
return false;
|
|
2034
|
+
}
|
|
2035
|
+
return (mimeType.startsWith("text/") ||
|
|
2036
|
+
fileName.endsWith(".txt") ||
|
|
2037
|
+
fileName.endsWith(".md") ||
|
|
2038
|
+
fileName.endsWith(".markdown") ||
|
|
2039
|
+
fileName.endsWith(".csv") ||
|
|
2040
|
+
fileName.endsWith(".json"));
|
|
2041
|
+
}
|
|
2042
|
+
function splitWikiIngestTextIntoChunks(sourceText, maxChars) {
|
|
2043
|
+
const text = sourceText.trim();
|
|
2044
|
+
if (text.length <= maxChars) {
|
|
2045
|
+
return [text];
|
|
2046
|
+
}
|
|
2047
|
+
const chunks = [];
|
|
2048
|
+
let start = 0;
|
|
2049
|
+
while (start < text.length) {
|
|
2050
|
+
const hardEnd = Math.min(text.length, start + maxChars);
|
|
2051
|
+
let end = hardEnd;
|
|
2052
|
+
if (hardEnd < text.length) {
|
|
2053
|
+
const newline = text.lastIndexOf("\n", hardEnd);
|
|
2054
|
+
if (newline > start + Math.floor(maxChars * 0.65)) {
|
|
2055
|
+
end = newline + 1;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const chunk = text.slice(start, end).trim();
|
|
2059
|
+
if (chunk.length > 0) {
|
|
2060
|
+
chunks.push(chunk);
|
|
2061
|
+
}
|
|
2062
|
+
if (end >= text.length) {
|
|
2063
|
+
break;
|
|
2064
|
+
}
|
|
2065
|
+
start = Math.max(0, end - WIKI_INGEST_TEXT_CHUNK_OVERLAP_CHARS);
|
|
2066
|
+
}
|
|
2067
|
+
return chunks;
|
|
2068
|
+
}
|
|
2069
|
+
async function splitLargeWikiIngestTextAsset(options) {
|
|
2070
|
+
if (!isChunkableWikiIngestTextAsset(options.asset)) {
|
|
2071
|
+
return 0;
|
|
2072
|
+
}
|
|
2073
|
+
const sourceText = await readFile(options.asset.file_path, "utf8");
|
|
2074
|
+
const chunks = splitWikiIngestTextIntoChunks(sourceText, MAX_WIKI_INGEST_TEXT_CHUNK_CHARS);
|
|
2075
|
+
if (chunks.length <= 1) {
|
|
2076
|
+
return 0;
|
|
2077
|
+
}
|
|
2078
|
+
const extension = path.extname(options.asset.file_name) || ".txt";
|
|
2079
|
+
const baseName = path.basename(options.asset.file_name, extension) ||
|
|
2080
|
+
options.asset.file_name ||
|
|
2081
|
+
"source";
|
|
2082
|
+
const width = String(chunks.length).length;
|
|
2083
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
2084
|
+
const chunkNumber = index + 1;
|
|
2085
|
+
const chunkFileName = `${baseName}-part-${String(chunkNumber).padStart(width, "0")}-of-${String(chunks.length).padStart(width, "0")}${extension}`;
|
|
2086
|
+
const chunkHeader = [
|
|
2087
|
+
`Source file: ${options.asset.file_name}`,
|
|
2088
|
+
`Source locator: ${options.asset.source_locator || options.asset.file_name}`,
|
|
2089
|
+
`Chunk: ${chunkNumber}/${chunks.length}`,
|
|
2090
|
+
`Parent checksum: ${options.asset.checksum}`,
|
|
2091
|
+
"",
|
|
2092
|
+
chunk
|
|
2093
|
+
].join("\n");
|
|
2094
|
+
const persisted = await persistIngestUpload({
|
|
2095
|
+
jobId: options.jobId,
|
|
2096
|
+
fileName: chunkFileName,
|
|
2097
|
+
mimeType: options.asset.mime_type || "text/plain",
|
|
2098
|
+
payload: Buffer.from(chunkHeader, "utf8")
|
|
2099
|
+
});
|
|
2100
|
+
createWikiIngestAssetRecord({
|
|
2101
|
+
jobId: options.jobId,
|
|
2102
|
+
sourceKind: "upload",
|
|
2103
|
+
sourceLocator: `${options.asset.source_locator || options.asset.file_name}#chunk-${chunkNumber}`,
|
|
2104
|
+
fileName: chunkFileName,
|
|
2105
|
+
mimeType: options.asset.mime_type || "text/plain",
|
|
2106
|
+
filePath: persisted.filePath,
|
|
2107
|
+
sizeBytes: persisted.sizeBytes,
|
|
2108
|
+
checksum: persisted.checksum,
|
|
2109
|
+
metadata: {
|
|
2110
|
+
chunkParentAssetId: options.asset.id,
|
|
2111
|
+
chunkParentChecksum: options.asset.checksum,
|
|
2112
|
+
chunkIndex: chunkNumber,
|
|
2113
|
+
chunkCount: chunks.length,
|
|
2114
|
+
chunkOverlapChars: WIKI_INGEST_TEXT_CHUNK_OVERLAP_CHARS,
|
|
2115
|
+
chunkMaxChars: MAX_WIKI_INGEST_TEXT_CHUNK_CHARS
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
updateWikiIngestAsset(options.asset.id, {
|
|
2120
|
+
status: "completed",
|
|
2121
|
+
metadata: {
|
|
2122
|
+
...parseJsonRecord(options.asset.metadata_json),
|
|
2123
|
+
textChunked: true,
|
|
2124
|
+
textChunkCount: chunks.length,
|
|
2125
|
+
textChunkMaxChars: MAX_WIKI_INGEST_TEXT_CHUNK_CHARS,
|
|
2126
|
+
textChunkOverlapChars: WIKI_INGEST_TEXT_CHUNK_OVERLAP_CHARS
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
return chunks.length;
|
|
2130
|
+
}
|
|
2020
2131
|
function getWikiIngestJobDir(jobId) {
|
|
2021
2132
|
return path.join(resolveDataDir(), "wiki-ingest", jobId);
|
|
2022
2133
|
}
|
|
@@ -2615,7 +2726,10 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2615
2726
|
const initialAssets = listWikiIngestJobAssetsInternal(jobId);
|
|
2616
2727
|
let processedFiles = initialAssets.filter((asset) => asset.status === "completed").length;
|
|
2617
2728
|
let totalFiles = Math.max(job.total_files, initialAssets.length);
|
|
2618
|
-
let hadSuccess =
|
|
2729
|
+
let hadSuccess = (() => {
|
|
2730
|
+
const counts = refreshCounts();
|
|
2731
|
+
return counts.pageCount + counts.entityCount > 0;
|
|
2732
|
+
})();
|
|
2619
2733
|
while (assetQueue().length > 0) {
|
|
2620
2734
|
const nextAsset = assetQueue().find((asset) => ["processing", "queued"].includes(asset.status));
|
|
2621
2735
|
if (!nextAsset) {
|
|
@@ -2677,6 +2791,27 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2677
2791
|
}
|
|
2678
2792
|
continue;
|
|
2679
2793
|
}
|
|
2794
|
+
const derivedChunkCount = await splitLargeWikiIngestTextAsset({
|
|
2795
|
+
jobId,
|
|
2796
|
+
asset: nextAsset
|
|
2797
|
+
});
|
|
2798
|
+
if (derivedChunkCount > 0) {
|
|
2799
|
+
totalFiles = Math.max(derivedChunkCount, totalFiles - 1 + derivedChunkCount);
|
|
2800
|
+
updateWikiIngestJob(jobId, {
|
|
2801
|
+
totalFiles,
|
|
2802
|
+
latestMessage: `Split ${nextAsset.file_name || "large text source"} into ${derivedChunkCount} chunks.`
|
|
2803
|
+
});
|
|
2804
|
+
createWikiIngestLog(jobId, `Split ${nextAsset.file_name || "large text source"} into ${derivedChunkCount} chunks.`, "info", {
|
|
2805
|
+
sourceAssetId: nextAsset.id,
|
|
2806
|
+
fileName: nextAsset.file_name,
|
|
2807
|
+
sourceLocator: nextAsset.source_locator,
|
|
2808
|
+
checksum: nextAsset.checksum,
|
|
2809
|
+
chunkCount: derivedChunkCount,
|
|
2810
|
+
chunkMaxChars: MAX_WIKI_INGEST_TEXT_CHUNK_CHARS,
|
|
2811
|
+
chunkOverlapChars: WIKI_INGEST_TEXT_CHUNK_OVERLAP_CHARS
|
|
2812
|
+
});
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2680
2815
|
updateWikiIngestAsset(nextAsset.id, { status: "processing" });
|
|
2681
2816
|
currentAssetContext = {
|
|
2682
2817
|
assetId: nextAsset.id,
|
|
@@ -301,6 +301,10 @@ function candidateIrohBinaries() {
|
|
|
301
301
|
return candidateIrohAssetRoots().flatMap((root) => [
|
|
302
302
|
path.join(root, "companion-iroh", "target", "release", binaryName),
|
|
303
303
|
path.join(root, "companion-iroh", "target", "debug", binaryName),
|
|
304
|
+
path.join(root, "companion-iroh-src", "target", "release", binaryName),
|
|
305
|
+
path.join(root, "companion-iroh-src", "target", "debug", binaryName),
|
|
306
|
+
path.join(root, "dist", "companion-iroh-src", "target", "release", binaryName),
|
|
307
|
+
path.join(root, "dist", "companion-iroh-src", "target", "debug", binaryName),
|
|
304
308
|
path.join(root, "openclaw-plugin", "dist", "companion-iroh", platformKey, binaryName),
|
|
305
309
|
path.join(root, "companion-iroh", platformKey, binaryName),
|
|
306
310
|
path.join(root, "companion-iroh", binaryName)
|
|
@@ -309,7 +313,8 @@ function candidateIrohBinaries() {
|
|
|
309
313
|
function resolveCompanionIrohManifestPath() {
|
|
310
314
|
const candidates = candidateIrohAssetRoots().flatMap((root) => [
|
|
311
315
|
path.join(root, "companion-iroh", "Cargo.toml"),
|
|
312
|
-
path.join(root, "companion-iroh-src", "Cargo.toml")
|
|
316
|
+
path.join(root, "companion-iroh-src", "Cargo.toml"),
|
|
317
|
+
path.join(root, "dist", "companion-iroh-src", "Cargo.toml")
|
|
313
318
|
]);
|
|
314
319
|
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
315
320
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "forge-openclaw-plugin",
|
|
3
3
|
"name": "Forge",
|
|
4
4
|
"description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.117",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|