forge-openclaw-plugin 0.2.113 → 0.2.115
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/companion-iroh/win32-x64/forge-companion-iroh.exe +0 -0
- package/dist/openclaw/local-runtime.js +86 -1
- package/dist/server/server/migrations/068_remove_generated_movement_wiki_logs.sql +12 -0
- package/dist/server/server/src/app.js +6 -2
- package/dist/server/server/src/discovery-advertiser.js +3 -7
- package/dist/server/server/src/managers/platform/openai-responses-provider.js +174 -32
- package/dist/server/server/src/movement.js +4 -198
- package/dist/server/server/src/repositories/wiki-memory.js +2 -0
- package/dist/server/server/src/services/companion-iroh.js +7 -3
- package/dist/server/server/src/web.js +11 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/068_remove_generated_movement_wiki_logs.sql +12 -0
|
Binary file
|
|
@@ -3,7 +3,7 @@ import { closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
const LOCAL_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
9
9
|
const STARTUP_TIMEOUT_MS = 15_000;
|
|
@@ -37,6 +37,12 @@ function getRuntimeStatePath(config) {
|
|
|
37
37
|
const origin = new URL(config.origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
38
38
|
return path.join(homedir(), ".openclaw", "run", FORGE_PLUGIN_ID, `${origin}-${config.port}.json`);
|
|
39
39
|
}
|
|
40
|
+
function getRuntimeStateDir() {
|
|
41
|
+
return path.join(homedir(), ".openclaw", "run", FORGE_PLUGIN_ID);
|
|
42
|
+
}
|
|
43
|
+
function getRuntimeStateOrigin(config) {
|
|
44
|
+
return new URL(config.origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
45
|
+
}
|
|
40
46
|
function getPreferredPortStatePath(origin) {
|
|
41
47
|
const hostname = new URL(origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
42
48
|
return path.join(homedir(), ".openclaw", "run", FORGE_PLUGIN_ID, `${hostname}-preferred-port.json`);
|
|
@@ -118,6 +124,10 @@ async function writeRuntimeState(config, pid) {
|
|
|
118
124
|
async function clearRuntimeState(config) {
|
|
119
125
|
await rm(getRuntimeStatePath(config), { force: true });
|
|
120
126
|
}
|
|
127
|
+
async function clearRuntimeStateForState(state) {
|
|
128
|
+
const origin = new URL(state.origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
129
|
+
await rm(path.join(getRuntimeStateDir(), `${origin}-${state.port}.json`), { force: true });
|
|
130
|
+
}
|
|
121
131
|
async function readRuntimeState(config) {
|
|
122
132
|
try {
|
|
123
133
|
const payload = await readFile(getRuntimeStatePath(config), "utf8");
|
|
@@ -138,6 +148,52 @@ async function readRuntimeState(config) {
|
|
|
138
148
|
return null;
|
|
139
149
|
}
|
|
140
150
|
}
|
|
151
|
+
async function readRuntimeStateFile(filePath) {
|
|
152
|
+
try {
|
|
153
|
+
const payload = await readFile(filePath, "utf8");
|
|
154
|
+
const parsed = JSON.parse(payload);
|
|
155
|
+
if (typeof parsed.pid !== "number" ||
|
|
156
|
+
!Number.isFinite(parsed.pid) ||
|
|
157
|
+
typeof parsed.origin !== "string" ||
|
|
158
|
+
typeof parsed.port !== "number" ||
|
|
159
|
+
!Number.isFinite(parsed.port)) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
pid: Math.trunc(parsed.pid),
|
|
164
|
+
origin: parsed.origin,
|
|
165
|
+
port: Math.trunc(parsed.port),
|
|
166
|
+
baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : buildForgeBaseUrl(parsed.origin, parsed.port),
|
|
167
|
+
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : new Date(0).toISOString(),
|
|
168
|
+
logPath: typeof parsed.logPath === "string" ? parsed.logPath : null
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function readRuntimeStatesForOrigin(config) {
|
|
176
|
+
const stateDir = getRuntimeStateDir();
|
|
177
|
+
const origin = getRuntimeStateOrigin(config);
|
|
178
|
+
let entries;
|
|
179
|
+
try {
|
|
180
|
+
entries = await readdir(stateDir);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
const states = [];
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
if (!entry.startsWith(`${origin}-`) || !entry.endsWith(".json") || entry.endsWith("-preferred-port.json")) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const state = await readRuntimeStateFile(path.join(stateDir, entry));
|
|
191
|
+
if (state) {
|
|
192
|
+
states.push(state);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return states;
|
|
196
|
+
}
|
|
141
197
|
function processExists(pid) {
|
|
142
198
|
try {
|
|
143
199
|
process.kill(pid, 0);
|
|
@@ -147,6 +203,34 @@ function processExists(pid) {
|
|
|
147
203
|
return !(error instanceof Error) || !("code" in error) || error.code !== "ESRCH";
|
|
148
204
|
}
|
|
149
205
|
}
|
|
206
|
+
async function cleanupSupersededManagedRuntimes(config, expectedDataRoot) {
|
|
207
|
+
const states = await readRuntimeStatesForOrigin(config);
|
|
208
|
+
for (const state of states) {
|
|
209
|
+
if (state.port === config.port) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (!processExists(state.pid)) {
|
|
213
|
+
await clearRuntimeStateForState(state);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const alternateConfig = {
|
|
217
|
+
...config,
|
|
218
|
+
port: state.port,
|
|
219
|
+
baseUrl: buildForgeBaseUrl(state.origin, state.port),
|
|
220
|
+
webAppUrl: buildForgeWebAppUrl(state.origin, state.port)
|
|
221
|
+
};
|
|
222
|
+
const alternateProbe = await probeForgeRuntime(alternateConfig, HEALTHCHECK_TIMEOUT_MS);
|
|
223
|
+
if (!alternateProbe.healthy || !isExpectedDataRoot(expectedDataRoot, alternateProbe.storageRoot)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
process.kill(state.pid, "SIGTERM");
|
|
227
|
+
if (!(await waitForProcessExit(state.pid, 5_000))) {
|
|
228
|
+
process.kill(state.pid, "SIGKILL");
|
|
229
|
+
await waitForProcessExit(state.pid, 2_000);
|
|
230
|
+
}
|
|
231
|
+
await clearRuntimeStateForState(state);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
150
234
|
async function waitForProcessExit(pid, timeoutMs) {
|
|
151
235
|
const deadline = Date.now() + timeoutMs;
|
|
152
236
|
while (Date.now() < deadline) {
|
|
@@ -490,6 +574,7 @@ export async function ensureForgeRuntimeReady(config) {
|
|
|
490
574
|
const expectedDataRoot = getExpectedDataRoot(config);
|
|
491
575
|
const initialProbe = await probeForgeRuntime(config, HEALTHCHECK_TIMEOUT_MS);
|
|
492
576
|
if (initialProbe.healthy && isExpectedDataRoot(expectedDataRoot, initialProbe.storageRoot)) {
|
|
577
|
+
await cleanupSupersededManagedRuntimes(config, expectedDataRoot);
|
|
493
578
|
const existingState = await readRuntimeState(config);
|
|
494
579
|
if (!existingState) {
|
|
495
580
|
await adoptManagedRuntimeState(config, initialProbe);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
UPDATE movement_stays
|
|
2
|
+
SET published_note_id = NULL
|
|
3
|
+
WHERE published_note_id IS NOT NULL;
|
|
4
|
+
|
|
5
|
+
UPDATE movement_trips
|
|
6
|
+
SET published_note_id = NULL
|
|
7
|
+
WHERE published_note_id IS NOT NULL;
|
|
8
|
+
|
|
9
|
+
DELETE FROM notes
|
|
10
|
+
WHERE source = 'system'
|
|
11
|
+
AND kind = 'evidence'
|
|
12
|
+
AND json_extract(frontmatter_json, '$.movement.kind') IN ('stay', 'trip');
|
|
@@ -5527,8 +5527,11 @@ function rewriteMountPath(url) {
|
|
|
5527
5527
|
const queryIndex = url.indexOf("?");
|
|
5528
5528
|
const pathname = queryIndex >= 0 ? url.slice(0, queryIndex) : url;
|
|
5529
5529
|
const search = queryIndex >= 0 ? url.slice(queryIndex) : "";
|
|
5530
|
+
if (pathname === "/") {
|
|
5531
|
+
return `/__forge-ui-root-redirect${search}`;
|
|
5532
|
+
}
|
|
5530
5533
|
if (pathname === "/forge") {
|
|
5531
|
-
return
|
|
5534
|
+
return `/__forge-ui-base-redirect${search}`;
|
|
5532
5535
|
}
|
|
5533
5536
|
if (pathname.startsWith("/forge/")) {
|
|
5534
5537
|
return `${pathname.slice("/forge".length) || "/"}${search}`;
|
|
@@ -9176,7 +9179,8 @@ export async function buildServer(options = {}) {
|
|
|
9176
9179
|
pages: listWikiPages({
|
|
9177
9180
|
spaceId: query.spaceId,
|
|
9178
9181
|
kind: query.kind,
|
|
9179
|
-
limit: query.limit ? Number(query.limit) : undefined
|
|
9182
|
+
limit: query.limit ? Number(query.limit) : undefined,
|
|
9183
|
+
includeHidden: query.includeHidden === "true"
|
|
9180
9184
|
})
|
|
9181
9185
|
};
|
|
9182
9186
|
});
|
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import bonjourService from "bonjour-service";
|
|
5
5
|
import { logForgeDebug } from "./debug.js";
|
|
6
|
-
import {
|
|
6
|
+
import { getCompanionIrohStatus } from "./services/companion-iroh.js";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
const BonjourConstructor = bonjourService.Bonjour ??
|
|
9
9
|
bonjourService.default ??
|
|
@@ -37,12 +37,8 @@ export async function startForgeDiscoveryAdvertiser(options) {
|
|
|
37
37
|
tsApiBaseUrl: tailscaleTargets.apiBaseUrl ?? "",
|
|
38
38
|
tsUiBaseUrl: tailscaleTargets.uiBaseUrl ?? "",
|
|
39
39
|
tsDnsName: tailscaleTargets.dnsName ?? "",
|
|
40
|
-
irohApiBaseUrl: irohNodeId
|
|
41
|
-
|
|
42
|
-
: "",
|
|
43
|
-
irohUiBaseUrl: irohNodeId
|
|
44
|
-
? companionIrohUiBaseUrlFromNodeId(irohNodeId)
|
|
45
|
-
: "",
|
|
40
|
+
irohApiBaseUrl: irohNodeId ? tailscaleTargets.apiBaseUrl ?? "" : "",
|
|
41
|
+
irohUiBaseUrl: irohNodeId ? tailscaleTargets.uiBaseUrl ?? "" : "",
|
|
46
42
|
irohProvider: irohNodeId ? "forge-companion-iroh" : "",
|
|
47
43
|
irohNodeId: irohNodeId ?? "",
|
|
48
44
|
irohRelay: irohTransport.pairPayload?.relay ?? "",
|
|
@@ -164,6 +164,106 @@ function parseOutputText(payload) {
|
|
|
164
164
|
}
|
|
165
165
|
return null;
|
|
166
166
|
}
|
|
167
|
+
function buildOutputTextPayload(text) {
|
|
168
|
+
return {
|
|
169
|
+
status: "completed",
|
|
170
|
+
output: [
|
|
171
|
+
{
|
|
172
|
+
content: [{ type: "output_text", text }]
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function parseCodexEventStreamPayload(streamText) {
|
|
178
|
+
const chunks = [];
|
|
179
|
+
let latestResponse = null;
|
|
180
|
+
let failedError = null;
|
|
181
|
+
let dataLines = [];
|
|
182
|
+
const flushEvent = () => {
|
|
183
|
+
if (dataLines.length === 0) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const raw = dataLines.join("\n");
|
|
187
|
+
dataLines = [];
|
|
188
|
+
if (raw.trim() === "[DONE]") {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
let payload;
|
|
192
|
+
try {
|
|
193
|
+
payload = JSON.parse(raw);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const payloadType = typeof payload.type === "string" ? payload.type : null;
|
|
199
|
+
if (payloadType === "response.output_text.delta") {
|
|
200
|
+
if (typeof payload.delta === "string") {
|
|
201
|
+
chunks.push(payload.delta);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (payloadType === "response.output_text.done") {
|
|
206
|
+
if (typeof payload.text === "string" && chunks.length === 0) {
|
|
207
|
+
chunks.push(payload.text);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (payloadType === "response.completed" ||
|
|
212
|
+
payloadType === "response.failed") {
|
|
213
|
+
const response = payload.response;
|
|
214
|
+
if (response && typeof response === "object") {
|
|
215
|
+
latestResponse = response;
|
|
216
|
+
const text = parseOutputText(latestResponse);
|
|
217
|
+
if (text && chunks.length === 0) {
|
|
218
|
+
chunks.push(text);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (payloadType === "response.failed") {
|
|
222
|
+
failedError =
|
|
223
|
+
payload.error ??
|
|
224
|
+
latestResponse?.error ??
|
|
225
|
+
"Codex response failed.";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
for (const line of streamText.split(/\r?\n/)) {
|
|
230
|
+
if (line === "") {
|
|
231
|
+
flushEvent();
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (line.startsWith(":")) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (line.startsWith("data:")) {
|
|
238
|
+
dataLines.push(line.slice("data:".length).trimStart());
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
flushEvent();
|
|
242
|
+
if (failedError) {
|
|
243
|
+
throw new Error(`Codex response failed: ${JSON.stringify(failedError)}`);
|
|
244
|
+
}
|
|
245
|
+
const text = chunks.join("");
|
|
246
|
+
if (latestResponse) {
|
|
247
|
+
if (!parseOutputText(latestResponse) && text) {
|
|
248
|
+
return buildOutputTextPayload(text);
|
|
249
|
+
}
|
|
250
|
+
return latestResponse;
|
|
251
|
+
}
|
|
252
|
+
if (text) {
|
|
253
|
+
return buildOutputTextPayload(text);
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return JSON.parse(streamText);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return buildOutputTextPayload("");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function readProviderPayload(response, profile) {
|
|
263
|
+
return isCodexProfile(profile)
|
|
264
|
+
? parseCodexEventStreamPayload(await response.text())
|
|
265
|
+
: readJsonPayload(response);
|
|
266
|
+
}
|
|
167
267
|
function readReasoningEffort(profile) {
|
|
168
268
|
return typeof profile.metadata.reasoningEffort === "string"
|
|
169
269
|
? profile.metadata.reasoningEffort
|
|
@@ -233,6 +333,7 @@ function buildRequestHeaders(profile, apiKey, options = {}) {
|
|
|
233
333
|
headers["OpenAI-Beta"] = "responses=experimental";
|
|
234
334
|
headers.originator = "pi";
|
|
235
335
|
headers["chatgpt-account-id"] = extractCodexAccountId(apiKey);
|
|
336
|
+
headers.accept = "text/event-stream";
|
|
236
337
|
return headers;
|
|
237
338
|
}
|
|
238
339
|
function buildReasoningConfiguration(profile) {
|
|
@@ -250,6 +351,9 @@ function buildTextConfiguration(options) {
|
|
|
250
351
|
}
|
|
251
352
|
return Object.keys(text).length > 0 ? text : undefined;
|
|
252
353
|
}
|
|
354
|
+
function buildInstructionsPayload(profile, instructions) {
|
|
355
|
+
return isCodexProfile(profile) ? { instructions } : {};
|
|
356
|
+
}
|
|
253
357
|
function estimateTokens(text) {
|
|
254
358
|
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
255
359
|
}
|
|
@@ -359,7 +463,11 @@ export class OpenAiResponsesProvider {
|
|
|
359
463
|
}),
|
|
360
464
|
body: JSON.stringify({
|
|
361
465
|
model: profile.model,
|
|
362
|
-
|
|
466
|
+
...buildInstructionsPayload(profile, "Reply with the single word ok."),
|
|
467
|
+
input: isCodexProfile(profile)
|
|
468
|
+
? "Connection test."
|
|
469
|
+
: "Reply with the single word ok.",
|
|
470
|
+
...(isCodexProfile(profile) ? { stream: true, store: false } : {}),
|
|
363
471
|
max_output_tokens: 24,
|
|
364
472
|
reasoning: buildReasoningConfiguration(profile),
|
|
365
473
|
text: buildTextConfiguration({ profile })
|
|
@@ -404,7 +512,7 @@ export class OpenAiResponsesProvider {
|
|
|
404
512
|
});
|
|
405
513
|
throw new Error(`OpenAI connection test failed (${response.status})${message ? `: ${message}` : ""}`);
|
|
406
514
|
}
|
|
407
|
-
const payload = await
|
|
515
|
+
const payload = await readProviderPayload(response, profile);
|
|
408
516
|
emitDiagnostic(logger, {
|
|
409
517
|
level: "info",
|
|
410
518
|
message: "OpenAI connection test completed.",
|
|
@@ -440,20 +548,31 @@ export class OpenAiResponsesProvider {
|
|
|
440
548
|
}),
|
|
441
549
|
body: JSON.stringify({
|
|
442
550
|
model: profile.model,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
551
|
+
...buildInstructionsPayload(profile, systemPrompt?.trim() || "Follow the user's request."),
|
|
552
|
+
input: isCodexProfile(profile)
|
|
553
|
+
? [
|
|
554
|
+
{
|
|
555
|
+
role: "user",
|
|
556
|
+
content: [{ type: "input_text", text: prompt }]
|
|
557
|
+
}
|
|
558
|
+
]
|
|
559
|
+
: [
|
|
560
|
+
...(systemPrompt?.trim()
|
|
561
|
+
? [
|
|
562
|
+
{
|
|
563
|
+
role: "system",
|
|
564
|
+
content: [
|
|
565
|
+
{ type: "input_text", text: systemPrompt.trim() }
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
]
|
|
569
|
+
: []),
|
|
570
|
+
{
|
|
571
|
+
role: "user",
|
|
572
|
+
content: [{ type: "input_text", text: prompt }]
|
|
573
|
+
}
|
|
574
|
+
],
|
|
575
|
+
...(isCodexProfile(profile) ? { stream: true, store: false } : {}),
|
|
457
576
|
reasoning: buildReasoningConfiguration(profile),
|
|
458
577
|
text: buildTextConfiguration({ profile }),
|
|
459
578
|
max_output_tokens: 1200
|
|
@@ -463,7 +582,7 @@ export class OpenAiResponsesProvider {
|
|
|
463
582
|
const message = await response.text();
|
|
464
583
|
throw new Error(`OpenAI text prompt failed (${response.status})${message ? `: ${message}` : ""}`);
|
|
465
584
|
}
|
|
466
|
-
const payload = await
|
|
585
|
+
const payload = await readProviderPayload(response, profile);
|
|
467
586
|
return {
|
|
468
587
|
outputText: parseOutputText(payload)?.trim() || ""
|
|
469
588
|
};
|
|
@@ -477,16 +596,16 @@ export class OpenAiResponsesProvider {
|
|
|
477
596
|
"Goal:",
|
|
478
597
|
"- Produce one main overview page in markdown.",
|
|
479
598
|
"- Split durable subtopics into articleCandidates with full draft markdown.",
|
|
480
|
-
"- Propose
|
|
599
|
+
"- Propose structured operational entities only when the source clearly supports a durable record.",
|
|
481
600
|
"- Suggest page updates only when the source clearly belongs in an existing page instead of a new page.",
|
|
482
601
|
"What Forge expects:",
|
|
483
602
|
"- The main markdown should be a readable overview or anchor page, not a raw source dump.",
|
|
484
603
|
"- articleCandidates should contain real draft wiki pages with their own markdown, not just titles.",
|
|
485
604
|
"- entityProposals must use one of these entityType values only: goal, project, task, habit, strategy, psyche_value, note.",
|
|
486
605
|
"- Use suggestedFields only for fields that truly fit the entity type. Use null for unknown scalar fields and [] for unknown list fields.",
|
|
487
|
-
"- Keep entity proposals conservative. Prefer wiki
|
|
606
|
+
"- Keep structured operational entity proposals conservative. Prefer wiki page entities when the source is informative but not actionable enough for goals, projects, tasks, habits, strategies, values, or evidence notes.",
|
|
488
607
|
"Forge ontology:",
|
|
489
|
-
"- Forge has two durable
|
|
608
|
+
"- Forge has two durable entity families: wiki page entities and structured operational entities.",
|
|
490
609
|
"- Use wiki pages for rich context, summaries, explanations, relationships, timelines, source synthesis, and themes that are broader than one action item.",
|
|
491
610
|
"- Use entities for operational objects that Forge can track directly: goals, projects, tasks, habits, strategies, psyche values, and durable notes.",
|
|
492
611
|
"- When the same topic needs both explanation and operations, create both: a wiki page for context and an entity proposal for the operational record.",
|
|
@@ -502,7 +621,17 @@ export class OpenAiResponsesProvider {
|
|
|
502
621
|
"- Keep markdown as the overview page for this source.",
|
|
503
622
|
"- If one topic deserves its own page, put it in articleCandidates with title, slug, summary, rationale, markdown, tags, aliases, and parentSlug.",
|
|
504
623
|
"- Use parentSlug when the draft page clearly belongs under an existing Forge wiki branch such as people, projects, concepts, sources, or chronicle.",
|
|
624
|
+
"- For chats and message logs, create articleCandidates for durable people, family members, partners, collaborators, sources, places, projects, important dated events, recurring themes, and concepts when the source contains enough reusable context to make a useful page.",
|
|
625
|
+
"- Do not leave durable people only as [[links]] when the source gives relationship, role, timeline, or follow-up context; create a lightweight person page candidate instead.",
|
|
626
|
+
"- Do not leave important or specific events only as bullets in the overview when they have their own timeline, decision, consequence, open loop, or later follow-up value; create a chronicle page candidate instead.",
|
|
627
|
+
"- Do not leave recurring concepts only as inline observations when the source gives definitions, examples, tradeoffs, or repeated behavior patterns; create a concept page candidate instead.",
|
|
505
628
|
"- Do not create articleCandidates for every minor mention; only create pages that would be useful to reopen later.",
|
|
629
|
+
"Coverage audit before output:",
|
|
630
|
+
"- Before final JSON, scan the source for page-worthy entities in these categories: people, family/partners, organizations, places, sources/files/links, dated events, decisions, recurring concepts, health/care episodes, projects/plans, and relationship contexts.",
|
|
631
|
+
"- For each category with enough durable context, either create an articleCandidate or make the reason for not splitting clear inside the overview page.",
|
|
632
|
+
"- For a close relationship chat, it is normal to create several page candidates: the source/export page, the main person page, close family/partner pages when named with context, and major event pages such as meetups, care crises, travel episodes, or important decisions.",
|
|
633
|
+
"- Prefer fewer high-quality pages over many stubs, but do not collapse distinct durable people or major events into one page merely because they came from the same file.",
|
|
634
|
+
"- If the source has a named spouse/partner, family member, collaborator, or recurring person with role/context/follow-up value, create a person page candidate even if the page is short.",
|
|
506
635
|
"Useful high-level wiki themes:",
|
|
507
636
|
"- people: people, collaborators, family, teams, roles, relationship context.",
|
|
508
637
|
"- projects: bounded initiatives, active efforts, plans, milestones, workstreams.",
|
|
@@ -528,7 +657,7 @@ export class OpenAiResponsesProvider {
|
|
|
528
657
|
"Entity proposal rules:",
|
|
529
658
|
"- Never invent IDs.",
|
|
530
659
|
"- Do not propose an entity just because it is mentioned once without durable importance.",
|
|
531
|
-
"- People, concepts, sources, places, and broad life areas are usually wiki
|
|
660
|
+
"- People, concepts, sources, places, and broad life areas are usually wiki page entities, not structured operational entity proposals.",
|
|
532
661
|
"- Goals should be outcomes, not chores.",
|
|
533
662
|
"- Projects should group multiple steps or phases, not a single errand.",
|
|
534
663
|
"- Tasks should be concrete and actionable enough to do soon.",
|
|
@@ -617,6 +746,8 @@ export class OpenAiResponsesProvider {
|
|
|
617
746
|
role: "user",
|
|
618
747
|
content: userContent
|
|
619
748
|
});
|
|
749
|
+
const requestInputs = isCodexProfile(profile) ? inputs.slice(1) : inputs;
|
|
750
|
+
const useStoredBackgroundResponse = !isCodexProfile(profile);
|
|
620
751
|
let payload;
|
|
621
752
|
let responseId = resumeResponseId?.trim() || null;
|
|
622
753
|
if (responseId) {
|
|
@@ -667,11 +798,17 @@ export class OpenAiResponsesProvider {
|
|
|
667
798
|
}),
|
|
668
799
|
body: JSON.stringify({
|
|
669
800
|
model: profile.model,
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
801
|
+
...buildInstructionsPayload(profile, prompt),
|
|
802
|
+
input: requestInputs,
|
|
803
|
+
store: useStoredBackgroundResponse,
|
|
804
|
+
...(isCodexProfile(profile) ? { stream: true } : {}),
|
|
805
|
+
...(useStoredBackgroundResponse
|
|
806
|
+
? {
|
|
807
|
+
background: true,
|
|
808
|
+
prompt_cache_retention: profile.model === "gpt-5.4" ? "24h" : "in_memory",
|
|
809
|
+
prompt_cache_key: `forge-wiki-ingest:${profile.model}:${input.parseStrategy}:${input.mimeType}`
|
|
810
|
+
}
|
|
811
|
+
: {}),
|
|
675
812
|
reasoning: buildReasoningConfiguration(profile),
|
|
676
813
|
text: buildTextConfiguration({
|
|
677
814
|
profile,
|
|
@@ -733,17 +870,21 @@ export class OpenAiResponsesProvider {
|
|
|
733
870
|
});
|
|
734
871
|
throw new Error(`LLM compilation failed: ${createResponse.status}${message ? `: ${message}` : ""}`);
|
|
735
872
|
}
|
|
736
|
-
payload = await
|
|
873
|
+
payload = await readProviderPayload(createResponse, profile);
|
|
737
874
|
responseId = readResponseId(payload);
|
|
738
|
-
if (!responseId) {
|
|
875
|
+
if (useStoredBackgroundResponse && !responseId) {
|
|
739
876
|
throw new Error("OpenAI background response did not include an id for polling.");
|
|
740
877
|
}
|
|
741
878
|
emitDiagnostic(logger, {
|
|
742
879
|
level: "info",
|
|
743
|
-
message:
|
|
880
|
+
message: useStoredBackgroundResponse
|
|
881
|
+
? "OpenAI accepted the wiki compilation job for background processing."
|
|
882
|
+
: "OpenAI Codex returned a foreground wiki compilation response.",
|
|
744
883
|
details: {
|
|
745
884
|
scope: "wiki_llm",
|
|
746
|
-
eventKey:
|
|
885
|
+
eventKey: useStoredBackgroundResponse
|
|
886
|
+
? "llm_compile_background_started"
|
|
887
|
+
: "llm_compile_foreground_completed",
|
|
747
888
|
provider: profile.provider,
|
|
748
889
|
baseUrl: profile.baseUrl,
|
|
749
890
|
model: profile.model,
|
|
@@ -755,7 +896,8 @@ export class OpenAiResponsesProvider {
|
|
|
755
896
|
}
|
|
756
897
|
let pollCount = 0;
|
|
757
898
|
let consecutivePollFailures = 0;
|
|
758
|
-
while (
|
|
899
|
+
while (useStoredBackgroundResponse &&
|
|
900
|
+
!isTerminalBackgroundStatus(readResponseStatus(payload))) {
|
|
759
901
|
await new Promise((resolve) => setTimeout(resolve, BACKGROUND_POLL_INTERVAL_MS));
|
|
760
902
|
try {
|
|
761
903
|
const pollResponse = await fetch(buildResponsesUrl(profile, responseId), {
|
|
@@ -838,7 +980,7 @@ export class OpenAiResponsesProvider {
|
|
|
838
980
|
}
|
|
839
981
|
}
|
|
840
982
|
const finalStatus = readResponseStatus(payload);
|
|
841
|
-
if (finalStatus !== "completed") {
|
|
983
|
+
if (finalStatus && finalStatus !== "completed") {
|
|
842
984
|
const errorMessage = readResponseError(payload) ??
|
|
843
985
|
`OpenAI background wiki compilation ended with status ${finalStatus}.`;
|
|
844
986
|
emitDiagnostic(logger, {
|
|
@@ -3,11 +3,10 @@ import { z } from "zod";
|
|
|
3
3
|
import { getDatabase } from "./db.js";
|
|
4
4
|
import { HttpError } from "./errors.js";
|
|
5
5
|
import { recordActivityEvent } from "./repositories/activity-events.js";
|
|
6
|
-
import {
|
|
6
|
+
import { getNoteById, updateNote } from "./repositories/notes.js";
|
|
7
7
|
import { createManualRewardGrant } from "./repositories/rewards.js";
|
|
8
8
|
import { listTaskRuns } from "./repositories/task-runs.js";
|
|
9
9
|
import { getDefaultUser } from "./repositories/users.js";
|
|
10
|
-
import { listWikiSpaces } from "./repositories/wiki-memory.js";
|
|
11
10
|
import { getScreenTimeOverlapSummary } from "./screen-time.js";
|
|
12
11
|
const movementPublishModeSchema = z.enum([
|
|
13
12
|
"auto_publish",
|
|
@@ -206,7 +205,7 @@ const movementTripInputSchema = z.object({
|
|
|
206
205
|
});
|
|
207
206
|
export const movementSettingsInputSchema = z.object({
|
|
208
207
|
trackingEnabled: z.boolean().default(false),
|
|
209
|
-
publishMode: movementPublishModeSchema.default("
|
|
208
|
+
publishMode: movementPublishModeSchema.default("draft_review"),
|
|
210
209
|
retentionMode: movementRetentionModeSchema.default("aggregates_only"),
|
|
211
210
|
locationPermissionStatus: z.string().trim().default("not_determined"),
|
|
212
211
|
motionPermissionStatus: z.string().trim().default("unknown"),
|
|
@@ -1030,7 +1029,7 @@ function defaultMovementSettings(userId) {
|
|
|
1030
1029
|
return {
|
|
1031
1030
|
userId,
|
|
1032
1031
|
trackingEnabled: false,
|
|
1033
|
-
publishMode: "
|
|
1032
|
+
publishMode: "draft_review",
|
|
1034
1033
|
retentionMode: "aggregates_only",
|
|
1035
1034
|
locationPermissionStatus: "not_determined",
|
|
1036
1035
|
motionPermissionStatus: "unknown",
|
|
@@ -1278,7 +1277,7 @@ function ensureMovementSettings(userId) {
|
|
|
1278
1277
|
background_tracking_ready, last_companion_sync_at, metadata_json,
|
|
1279
1278
|
created_at, updated_at
|
|
1280
1279
|
)
|
|
1281
|
-
VALUES (?, 0, '
|
|
1280
|
+
VALUES (?, 0, 'draft_review', 'aggregates_only', 'not_determined', 'unknown', 0, NULL, '{}', ?, ?)`)
|
|
1282
1281
|
.run(userId, now, now);
|
|
1283
1282
|
return getMovementSettingsRow(userId);
|
|
1284
1283
|
}
|
|
@@ -1363,9 +1362,6 @@ function listTripStops(tripIds) {
|
|
|
1363
1362
|
ORDER BY trip_id ASC, sequence_index ASC`)
|
|
1364
1363
|
.all(...tripIds);
|
|
1365
1364
|
}
|
|
1366
|
-
function defaultSpaceId() {
|
|
1367
|
-
return listWikiSpaces()[0]?.id;
|
|
1368
|
-
}
|
|
1369
1365
|
function syncPlaceWikiMetadata(placeId) {
|
|
1370
1366
|
const row = getDatabase()
|
|
1371
1367
|
.prepare(`SELECT *
|
|
@@ -1561,176 +1557,6 @@ function resolvePlaceForPatch(input) {
|
|
|
1561
1557
|
}
|
|
1562
1558
|
return undefined;
|
|
1563
1559
|
}
|
|
1564
|
-
function createMovementNote(input) {
|
|
1565
|
-
const spaceId = defaultSpaceId();
|
|
1566
|
-
if (!spaceId) {
|
|
1567
|
-
return null;
|
|
1568
|
-
}
|
|
1569
|
-
return createNote({
|
|
1570
|
-
kind: "evidence",
|
|
1571
|
-
title: input.title,
|
|
1572
|
-
slug: "",
|
|
1573
|
-
summary: "",
|
|
1574
|
-
contentMarkdown: input.contentMarkdown,
|
|
1575
|
-
spaceId,
|
|
1576
|
-
parentSlug: null,
|
|
1577
|
-
indexOrder: 0,
|
|
1578
|
-
showInIndex: false,
|
|
1579
|
-
aliases: [],
|
|
1580
|
-
userId: input.userId,
|
|
1581
|
-
author: null,
|
|
1582
|
-
links: [],
|
|
1583
|
-
tags: input.tags,
|
|
1584
|
-
destroyAt: null,
|
|
1585
|
-
sourcePath: "",
|
|
1586
|
-
frontmatter: input.frontmatter,
|
|
1587
|
-
revisionHash: ""
|
|
1588
|
-
}, { actor: "Movement sync", source: "system" });
|
|
1589
|
-
}
|
|
1590
|
-
function formatMovementDurationForNote(valueSeconds) {
|
|
1591
|
-
if (valueSeconds >= 86_400) {
|
|
1592
|
-
return `${round(valueSeconds / 86_400, 1)} days`;
|
|
1593
|
-
}
|
|
1594
|
-
if (valueSeconds >= 3_600) {
|
|
1595
|
-
return `${round(valueSeconds / 3_600, 1)} hours`;
|
|
1596
|
-
}
|
|
1597
|
-
return `${Math.max(1, Math.round(valueSeconds / 60))} minutes`;
|
|
1598
|
-
}
|
|
1599
|
-
function mergeMovementNoteTags(existingTags, existingFrontmatter, generatedTags) {
|
|
1600
|
-
const movement = existingFrontmatter.movement &&
|
|
1601
|
-
typeof existingFrontmatter.movement === "object" &&
|
|
1602
|
-
!Array.isArray(existingFrontmatter.movement)
|
|
1603
|
-
? existingFrontmatter.movement
|
|
1604
|
-
: null;
|
|
1605
|
-
const previousGeneratedTags = Array.isArray(movement?.generatedTags)
|
|
1606
|
-
? movement.generatedTags.filter((value) => typeof value === "string")
|
|
1607
|
-
: [];
|
|
1608
|
-
const previousGeneratedTagSet = new Set(previousGeneratedTags.map((tag) => tag.toLowerCase()));
|
|
1609
|
-
const preservedTags = existingTags.filter((tag) => !previousGeneratedTagSet.has(tag.toLowerCase()));
|
|
1610
|
-
return uniqStrings([...preservedTags, ...generatedTags]);
|
|
1611
|
-
}
|
|
1612
|
-
function syncMovementNote(input) {
|
|
1613
|
-
const existingNote = input.publishedNoteId
|
|
1614
|
-
? getNoteById(input.publishedNoteId)
|
|
1615
|
-
: null;
|
|
1616
|
-
if (existingNote && !Array.isArray(existingNote)) {
|
|
1617
|
-
const updated = updateNote(existingNote.id, {
|
|
1618
|
-
title: input.title,
|
|
1619
|
-
contentMarkdown: input.contentMarkdown,
|
|
1620
|
-
tags: mergeMovementNoteTags(existingNote.tags ?? [], existingNote.frontmatter, input.generatedTags),
|
|
1621
|
-
frontmatter: {
|
|
1622
|
-
...existingNote.frontmatter,
|
|
1623
|
-
...input.frontmatter
|
|
1624
|
-
}
|
|
1625
|
-
}, { actor: "Movement sync", source: "system" });
|
|
1626
|
-
return updated?.id ?? existingNote.id;
|
|
1627
|
-
}
|
|
1628
|
-
const created = createMovementNote({
|
|
1629
|
-
userId: input.userId,
|
|
1630
|
-
title: input.title,
|
|
1631
|
-
contentMarkdown: input.contentMarkdown,
|
|
1632
|
-
tags: input.generatedTags,
|
|
1633
|
-
frontmatter: input.frontmatter
|
|
1634
|
-
});
|
|
1635
|
-
return created?.id ?? null;
|
|
1636
|
-
}
|
|
1637
|
-
function syncStayNote(settings, stay, place) {
|
|
1638
|
-
if (!settings || settings.publishMode === "no_publish") {
|
|
1639
|
-
return null;
|
|
1640
|
-
}
|
|
1641
|
-
const label = place?.label || stay.label || "Unlabeled stay";
|
|
1642
|
-
const durationSecondsValue = durationSeconds(stay.started_at, stay.ended_at);
|
|
1643
|
-
const live = stay.status.trim().toLowerCase() !== "completed" &&
|
|
1644
|
-
stay.status.trim().toLowerCase() !== "closed";
|
|
1645
|
-
const generatedTags = uniqStrings([
|
|
1646
|
-
"movement",
|
|
1647
|
-
"stay",
|
|
1648
|
-
...(place ? safeJsonParse(place.category_tags_json, []) : [])
|
|
1649
|
-
]);
|
|
1650
|
-
const content = [
|
|
1651
|
-
live ? `Currently staying at **${label}**.` : `Stayed at **${label}**.`,
|
|
1652
|
-
"",
|
|
1653
|
-
`- Started: ${stay.started_at}`,
|
|
1654
|
-
`- ${live ? "Current end" : "Ended"}: ${stay.ended_at}`,
|
|
1655
|
-
`- Duration: ${formatMovementDurationForNote(durationSecondsValue)}`,
|
|
1656
|
-
`- Radius: ${Math.round(stay.radius_meters)} m`,
|
|
1657
|
-
`- Classification: ${stay.classification || "stationary"}`
|
|
1658
|
-
].join("\n");
|
|
1659
|
-
return syncMovementNote({
|
|
1660
|
-
userId: stay.user_id,
|
|
1661
|
-
publishedNoteId: stay.published_note_id,
|
|
1662
|
-
title: `Stay · ${label}`,
|
|
1663
|
-
contentMarkdown: content,
|
|
1664
|
-
generatedTags,
|
|
1665
|
-
frontmatter: {
|
|
1666
|
-
observedAt: stay.started_at,
|
|
1667
|
-
movement: {
|
|
1668
|
-
kind: "stay",
|
|
1669
|
-
state: live ? "live" : "closed",
|
|
1670
|
-
stayId: stay.id,
|
|
1671
|
-
publishMode: settings.publishMode,
|
|
1672
|
-
placeId: place?.id ?? null,
|
|
1673
|
-
placeLabel: label,
|
|
1674
|
-
startedAt: stay.started_at,
|
|
1675
|
-
endedAt: stay.ended_at,
|
|
1676
|
-
durationSeconds: durationSecondsValue,
|
|
1677
|
-
generatedTags
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
});
|
|
1681
|
-
}
|
|
1682
|
-
function syncTripNote(settings, trip, startPlace, endPlace) {
|
|
1683
|
-
if (!settings || settings.publishMode === "no_publish") {
|
|
1684
|
-
return null;
|
|
1685
|
-
}
|
|
1686
|
-
const startLabel = startPlace?.label || "Unknown start";
|
|
1687
|
-
const endLabel = endPlace?.label || "Unknown end";
|
|
1688
|
-
const durationSecondsValue = durationSeconds(trip.started_at, trip.ended_at);
|
|
1689
|
-
const distanceKm = round(trip.distance_meters / 1000, 2);
|
|
1690
|
-
const live = trip.status.trim().toLowerCase() !== "completed" &&
|
|
1691
|
-
trip.status.trim().toLowerCase() !== "closed";
|
|
1692
|
-
const generatedTags = uniqStrings([
|
|
1693
|
-
"movement",
|
|
1694
|
-
"trip",
|
|
1695
|
-
...safeJsonParse(trip.tags_json, [])
|
|
1696
|
-
]);
|
|
1697
|
-
const content = [
|
|
1698
|
-
live
|
|
1699
|
-
? `Currently moving from **${startLabel}** to **${endLabel}**.`
|
|
1700
|
-
: `Travelled from **${startLabel}** to **${endLabel}**.`,
|
|
1701
|
-
"",
|
|
1702
|
-
`- Started: ${trip.started_at}`,
|
|
1703
|
-
`- ${live ? "Current end" : "Ended"}: ${trip.ended_at}`,
|
|
1704
|
-
`- Duration: ${formatMovementDurationForNote(durationSecondsValue)}`,
|
|
1705
|
-
`- Distance: ${distanceKm} km`,
|
|
1706
|
-
`- Activity: ${trip.activity_type || trip.travel_mode}`
|
|
1707
|
-
].join("\n");
|
|
1708
|
-
return syncMovementNote({
|
|
1709
|
-
userId: trip.user_id,
|
|
1710
|
-
publishedNoteId: trip.published_note_id,
|
|
1711
|
-
title: `Trip · ${startLabel} → ${endLabel}`,
|
|
1712
|
-
contentMarkdown: content,
|
|
1713
|
-
generatedTags,
|
|
1714
|
-
frontmatter: {
|
|
1715
|
-
observedAt: trip.started_at,
|
|
1716
|
-
movement: {
|
|
1717
|
-
kind: "trip",
|
|
1718
|
-
state: live ? "live" : "closed",
|
|
1719
|
-
tripId: trip.id,
|
|
1720
|
-
publishMode: settings.publishMode,
|
|
1721
|
-
startPlaceId: startPlace?.id ?? null,
|
|
1722
|
-
endPlaceId: endPlace?.id ?? null,
|
|
1723
|
-
startPlaceLabel: startLabel,
|
|
1724
|
-
endPlaceLabel: endLabel,
|
|
1725
|
-
startedAt: trip.started_at,
|
|
1726
|
-
endedAt: trip.ended_at,
|
|
1727
|
-
durationSeconds: durationSecondsValue,
|
|
1728
|
-
distanceMeters: trip.distance_meters,
|
|
1729
|
-
generatedTags
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
1560
|
function awardMovementXp(input) {
|
|
1735
1561
|
const deltaXp = estimateMovementXp(input.categoryTags, input.distanceMeters);
|
|
1736
1562
|
if (deltaXp <= 0) {
|
|
@@ -1905,16 +1731,6 @@ function upsertMovementStay(pairing, settings, input) {
|
|
|
1905
1731
|
.prepare(`SELECT * FROM movement_stays WHERE user_id = ? AND external_uid = ?`)
|
|
1906
1732
|
.get(pairing.user_id, parsed.externalUid);
|
|
1907
1733
|
const freshMetadata = safeJsonParse(fresh.metadata_json, {});
|
|
1908
|
-
if (settings?.publishMode === "auto_publish" && !hasInvalidMovementRecord(freshMetadata)) {
|
|
1909
|
-
const publishedNoteId = syncStayNote(settings, fresh, matchedPlace);
|
|
1910
|
-
if (publishedNoteId && publishedNoteId !== fresh.published_note_id) {
|
|
1911
|
-
getDatabase()
|
|
1912
|
-
.prepare(`UPDATE movement_stays
|
|
1913
|
-
SET published_note_id = ?, updated_at = ?
|
|
1914
|
-
WHERE id = ?`)
|
|
1915
|
-
.run(publishedNoteId, nowIso(), fresh.id);
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
1734
|
return {
|
|
1919
1735
|
mode: existing ? "updated" : "created",
|
|
1920
1736
|
stayId: fresh.id
|
|
@@ -2040,16 +1856,6 @@ function upsertMovementTrip(pairing, settings, input) {
|
|
|
2040
1856
|
.prepare(`SELECT * FROM movement_trips WHERE id = ?`)
|
|
2041
1857
|
.get(fresh.id);
|
|
2042
1858
|
const freshMetadata = safeJsonParse(refreshed.metadata_json, {});
|
|
2043
|
-
if (settings?.publishMode === "auto_publish" && !hasInvalidMovementRecord(freshMetadata)) {
|
|
2044
|
-
const publishedNoteId = syncTripNote(settings, refreshed, startPlace, endPlace);
|
|
2045
|
-
if (publishedNoteId && publishedNoteId !== refreshed.published_note_id) {
|
|
2046
|
-
getDatabase()
|
|
2047
|
-
.prepare(`UPDATE movement_trips
|
|
2048
|
-
SET published_note_id = ?, updated_at = ?
|
|
2049
|
-
WHERE id = ?`)
|
|
2050
|
-
.run(publishedNoteId, nowIso(), refreshed.id);
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
1859
|
if (!existing && settings?.publishMode === "auto_publish" && !hasInvalidMovementRecord(freshMetadata)) {
|
|
2054
1860
|
awardMovementXp({
|
|
2055
1861
|
userId: pairing.user_id,
|
|
@@ -1515,6 +1515,7 @@ export function listWikiPages(query) {
|
|
|
1515
1515
|
ensureWikiSpaceSeedPages(spaceId);
|
|
1516
1516
|
return listAllNotes()
|
|
1517
1517
|
.filter((note) => note.spaceId === spaceId)
|
|
1518
|
+
.filter((note) => (query.includeHidden ? true : note.showInIndex))
|
|
1518
1519
|
.filter((note) => (query.kind ? note.kind === query.kind : true))
|
|
1519
1520
|
.sort(compareWikiPageOrder)
|
|
1520
1521
|
.slice(0, query.limit ?? 100);
|
|
@@ -1638,6 +1639,7 @@ export async function searchWikiPages(input, secrets) {
|
|
|
1638
1639
|
const parsed = wikiSearchQuerySchema.parse(input);
|
|
1639
1640
|
const pages = listAllNotes()
|
|
1640
1641
|
.filter((page) => (parsed.spaceId ? page.spaceId === parsed.spaceId : true))
|
|
1642
|
+
.filter((page) => page.showInIndex)
|
|
1641
1643
|
.filter((page) => (parsed.kind ? page.kind === parsed.kind : true));
|
|
1642
1644
|
const scores = new Map();
|
|
1643
1645
|
const addScore = (noteId, value) => {
|
|
@@ -30,10 +30,13 @@ export async function buildCompanionPairingTransport(input) {
|
|
|
30
30
|
pairPayload: snapshot.pairPayload,
|
|
31
31
|
alpn: snapshot.alpn ?? COMPANION_IROH_ALPN,
|
|
32
32
|
localBaseUrl: snapshot.localBaseUrl,
|
|
33
|
+
fallbackApiBaseUrl: requestApiBaseUrl,
|
|
34
|
+
fallbackUiBaseUrl: requestUiBaseUrl,
|
|
33
35
|
recreateCommand: snapshot.recreateCommand ?? undefined,
|
|
34
36
|
startedAt: snapshot.startedAt ?? undefined,
|
|
35
37
|
notes: [
|
|
36
|
-
"Default pairing uses Forge's Rust Iroh transport over QUIC.",
|
|
38
|
+
"Default pairing uses Forge's Rust Iroh transport over QUIC first.",
|
|
39
|
+
"The QR keeps the request API/UI URL as a direct fallback when Iroh cannot complete a request.",
|
|
37
40
|
"The QR payload carries the Iroh node id, host token, optional relay, and ALPN forge-companion/1.",
|
|
38
41
|
"Manual HTTP/TCP pairing remains available with --manual-http for advanced local setups."
|
|
39
42
|
]
|
|
@@ -200,12 +203,13 @@ function irohTransport(input) {
|
|
|
200
203
|
const nodeId = input.pairPayload.node_id;
|
|
201
204
|
return {
|
|
202
205
|
transportMode: "iroh",
|
|
203
|
-
apiBaseUrl:
|
|
204
|
-
uiBaseUrl:
|
|
206
|
+
apiBaseUrl: input.fallbackApiBaseUrl,
|
|
207
|
+
uiBaseUrl: input.fallbackUiBaseUrl,
|
|
205
208
|
transport: {
|
|
206
209
|
protocol: "iroh",
|
|
207
210
|
provider: "forge-companion-iroh",
|
|
208
211
|
status: "ready",
|
|
212
|
+
publicBaseUrl: input.fallbackApiBaseUrl,
|
|
209
213
|
localBaseUrl: input.localBaseUrl,
|
|
210
214
|
nodeId,
|
|
211
215
|
relay: input.pairPayload.relay,
|
|
@@ -487,6 +487,7 @@ export async function registerWebRoutes(app, options = {}) {
|
|
|
487
487
|
const devWebRuntime = options.devWebRuntime ?? createManagedDevWebRuntime();
|
|
488
488
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
489
489
|
const devAssetProxy = options.devAssetProxy ?? createDevAssetProxy(fetchImpl);
|
|
490
|
+
const basePath = normalizeBasePath(getDefaultBasePath());
|
|
490
491
|
app.addHook("onClose", async () => {
|
|
491
492
|
await devWebRuntime.stop();
|
|
492
493
|
devAssetProxy.close();
|
|
@@ -501,6 +502,16 @@ export async function registerWebRoutes(app, options = {}) {
|
|
|
501
502
|
});
|
|
502
503
|
})();
|
|
503
504
|
});
|
|
505
|
+
app.get("/__forge-ui-root-redirect", async (_request, reply) => {
|
|
506
|
+
if (basePath !== "/") {
|
|
507
|
+
reply.redirect(basePath, 302);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
return serveAsset("/", reply, { devWebRuntime, devAssetProxy });
|
|
511
|
+
});
|
|
512
|
+
app.get("/__forge-ui-base-redirect", async (_request, reply) => {
|
|
513
|
+
reply.redirect(basePath, 302);
|
|
514
|
+
});
|
|
504
515
|
app.get("/", async (_request, reply) => serveAsset("/", reply, { devWebRuntime, devAssetProxy }));
|
|
505
516
|
app.get("/*", async (request, reply) => serveAsset(request.url, reply, { devWebRuntime, devAssetProxy }));
|
|
506
517
|
}
|
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.115",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
UPDATE movement_stays
|
|
2
|
+
SET published_note_id = NULL
|
|
3
|
+
WHERE published_note_id IS NOT NULL;
|
|
4
|
+
|
|
5
|
+
UPDATE movement_trips
|
|
6
|
+
SET published_note_id = NULL
|
|
7
|
+
WHERE published_note_id IS NOT NULL;
|
|
8
|
+
|
|
9
|
+
DELETE FROM notes
|
|
10
|
+
WHERE source = 'system'
|
|
11
|
+
AND kind = 'evidence'
|
|
12
|
+
AND json_extract(frontmatter_json, '$.movement.kind') IN ('stay', 'trip');
|