forge-openclaw-plugin 0.2.20 → 0.2.22
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/assets/{board-DGbXWEuu.js → board-_C6oMy5w.js} +2 -2
- package/dist/assets/{board-DGbXWEuu.js.map → board-_C6oMy5w.js.map} +1 -1
- package/dist/assets/index-Ch_xeZ2u.js +63 -0
- package/dist/assets/index-Ch_xeZ2u.js.map +1 -0
- package/dist/assets/index-DvVM7K6j.css +1 -0
- package/dist/assets/{motion-B5Qoz2Ci.js → motion-D4sZgCHd.js} +2 -2
- package/dist/assets/{motion-B5Qoz2Ci.js.map → motion-D4sZgCHd.js.map} +1 -1
- package/dist/assets/{table-D_iurDQu.js → table-BWzTaky1.js} +2 -2
- package/dist/assets/{table-D_iurDQu.js.map → table-BWzTaky1.js.map} +1 -1
- package/dist/assets/{ui-D5QUYUq4.js → ui-BzK4azQb.js} +2 -2
- package/dist/assets/{ui-D5QUYUq4.js.map → ui-BzK4azQb.js.map} +1 -1
- package/dist/assets/vendor-De38P6YR.js +729 -0
- package/dist/assets/vendor-De38P6YR.js.map +1 -0
- package/dist/assets/{viz-BD9WSxHz.js → viz-C6hfyqzu.js} +2 -2
- package/dist/assets/{viz-BD9WSxHz.js.map → viz-C6hfyqzu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/server/app.js +328 -19
- package/dist/server/health.js +82 -21
- package/dist/server/managers/platform/background-job-manager.js +103 -8
- package/dist/server/managers/platform/llm-manager.js +91 -5
- package/dist/server/managers/platform/openai-responses-provider.js +683 -70
- package/dist/server/repositories/diagnostic-logs.js +243 -0
- package/dist/server/repositories/wiki-memory.js +619 -66
- package/dist/server/types.js +56 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/023_diagnostic_logs.sql +28 -0
- package/skills/forge-openclaw/SKILL.md +14 -0
- package/dist/assets/index-4-1WI9i7.css +0 -1
- package/dist/assets/index-BZbHajNK.js +0 -63
- package/dist/assets/index-BZbHajNK.js.map +0 -1
- package/dist/assets/vendor-KARp8LAR.js +0 -706
- package/dist/assets/vendor-KARp8LAR.js.map +0 -1
|
@@ -1,3 +1,143 @@
|
|
|
1
|
+
function emitDiagnostic(logger, input) {
|
|
2
|
+
logger?.(input);
|
|
3
|
+
}
|
|
4
|
+
function truncate(value, limit = 1_600) {
|
|
5
|
+
return value.length > limit ? `${value.slice(0, limit)}…` : value;
|
|
6
|
+
}
|
|
7
|
+
const SUPPORTED_INGEST_ENTITY_TYPES = [
|
|
8
|
+
"goal",
|
|
9
|
+
"project",
|
|
10
|
+
"task",
|
|
11
|
+
"habit",
|
|
12
|
+
"strategy",
|
|
13
|
+
"psyche_value",
|
|
14
|
+
"note"
|
|
15
|
+
];
|
|
16
|
+
const MODEL_CONTEXT_WINDOWS = {
|
|
17
|
+
"gpt-5.4": 1_050_000,
|
|
18
|
+
"gpt-5.4-mini": 400_000,
|
|
19
|
+
"gpt-5.4-nano": 400_000
|
|
20
|
+
};
|
|
21
|
+
const DEFAULT_CONTEXT_WINDOW = 400_000;
|
|
22
|
+
const RESERVED_RESPONSE_TOKENS = 140_000;
|
|
23
|
+
const APPROX_CHARS_PER_TOKEN = 4;
|
|
24
|
+
const REQUEST_TIMEOUT_MS = 90_000;
|
|
25
|
+
const BACKGROUND_POLL_INTERVAL_MS = 2_000;
|
|
26
|
+
function closedObject(properties) {
|
|
27
|
+
return {
|
|
28
|
+
type: "object",
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
properties,
|
|
31
|
+
required: Object.keys(properties)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function stringArraySchema() {
|
|
35
|
+
return {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: { type: "string" }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function integerArraySchema() {
|
|
41
|
+
return {
|
|
42
|
+
type: "array",
|
|
43
|
+
items: { type: "integer" }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function nullableStringSchema() {
|
|
47
|
+
return {
|
|
48
|
+
type: ["string", "null"]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function nullableNumberSchema() {
|
|
52
|
+
return {
|
|
53
|
+
type: ["number", "null"]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function buildWikiIngestSchema() {
|
|
57
|
+
const linkedEntitySchema = closedObject({
|
|
58
|
+
entityType: {
|
|
59
|
+
type: "string",
|
|
60
|
+
enum: ["goal", "project", "task", "habit", "strategy", "psyche_value"]
|
|
61
|
+
},
|
|
62
|
+
entityId: { type: "string" },
|
|
63
|
+
rationale: nullableStringSchema()
|
|
64
|
+
});
|
|
65
|
+
const suggestedFieldsSchema = closedObject({
|
|
66
|
+
goalId: nullableStringSchema(),
|
|
67
|
+
projectId: nullableStringSchema(),
|
|
68
|
+
horizon: nullableStringSchema(),
|
|
69
|
+
status: nullableStringSchema(),
|
|
70
|
+
priority: nullableStringSchema(),
|
|
71
|
+
dueDate: nullableStringSchema(),
|
|
72
|
+
themeColor: nullableStringSchema(),
|
|
73
|
+
polarity: nullableStringSchema(),
|
|
74
|
+
frequency: nullableStringSchema(),
|
|
75
|
+
endStateDescription: nullableStringSchema(),
|
|
76
|
+
valuedDirection: nullableStringSchema(),
|
|
77
|
+
whyItMatters: nullableStringSchema(),
|
|
78
|
+
userId: nullableStringSchema(),
|
|
79
|
+
targetPoints: nullableNumberSchema(),
|
|
80
|
+
estimatedMinutes: nullableNumberSchema(),
|
|
81
|
+
targetCount: nullableNumberSchema(),
|
|
82
|
+
rewardXp: nullableNumberSchema(),
|
|
83
|
+
penaltyXp: nullableNumberSchema(),
|
|
84
|
+
linkedGoalIds: stringArraySchema(),
|
|
85
|
+
linkedProjectIds: stringArraySchema(),
|
|
86
|
+
linkedTaskIds: stringArraySchema(),
|
|
87
|
+
linkedValueIds: stringArraySchema(),
|
|
88
|
+
targetGoalIds: stringArraySchema(),
|
|
89
|
+
targetProjectIds: stringArraySchema(),
|
|
90
|
+
weekDays: integerArraySchema(),
|
|
91
|
+
linkedEntities: {
|
|
92
|
+
type: "array",
|
|
93
|
+
items: linkedEntitySchema
|
|
94
|
+
},
|
|
95
|
+
committedActions: stringArraySchema(),
|
|
96
|
+
notes: stringArraySchema(),
|
|
97
|
+
tags: stringArraySchema()
|
|
98
|
+
});
|
|
99
|
+
return closedObject({
|
|
100
|
+
title: { type: "string" },
|
|
101
|
+
summary: { type: "string" },
|
|
102
|
+
markdown: { type: "string" },
|
|
103
|
+
tags: stringArraySchema(),
|
|
104
|
+
entityProposals: {
|
|
105
|
+
type: "array",
|
|
106
|
+
items: closedObject({
|
|
107
|
+
entityType: {
|
|
108
|
+
type: "string",
|
|
109
|
+
enum: [...SUPPORTED_INGEST_ENTITY_TYPES]
|
|
110
|
+
},
|
|
111
|
+
title: { type: "string" },
|
|
112
|
+
summary: { type: "string" },
|
|
113
|
+
rationale: { type: "string" },
|
|
114
|
+
confidence: { type: "number" },
|
|
115
|
+
suggestedFields: suggestedFieldsSchema
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
pageUpdateSuggestions: {
|
|
119
|
+
type: "array",
|
|
120
|
+
items: closedObject({
|
|
121
|
+
targetSlug: { type: "string" },
|
|
122
|
+
rationale: { type: "string" },
|
|
123
|
+
patchSummary: { type: "string" }
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
articleCandidates: {
|
|
127
|
+
type: "array",
|
|
128
|
+
items: closedObject({
|
|
129
|
+
title: { type: "string" },
|
|
130
|
+
slug: { type: "string" },
|
|
131
|
+
parentSlug: nullableStringSchema(),
|
|
132
|
+
rationale: { type: "string" },
|
|
133
|
+
summary: { type: "string" },
|
|
134
|
+
markdown: { type: "string" },
|
|
135
|
+
tags: stringArraySchema(),
|
|
136
|
+
aliases: stringArraySchema()
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
1
141
|
function isOutputTextPart(part) {
|
|
2
142
|
return (part !== null &&
|
|
3
143
|
typeof part === "object" &&
|
|
@@ -5,7 +145,7 @@ function isOutputTextPart(part) {
|
|
|
5
145
|
part.type === "output_text" &&
|
|
6
146
|
typeof part.text === "string");
|
|
7
147
|
}
|
|
8
|
-
function
|
|
148
|
+
function parseOutputText(payload) {
|
|
9
149
|
const output = Array.isArray(payload.output) ? payload.output : [];
|
|
10
150
|
for (const item of output) {
|
|
11
151
|
if (!item || typeof item !== "object") {
|
|
@@ -22,6 +162,82 @@ function parseJsonFromOutput(payload) {
|
|
|
22
162
|
}
|
|
23
163
|
return null;
|
|
24
164
|
}
|
|
165
|
+
function readReasoningEffort(profile) {
|
|
166
|
+
return typeof profile.metadata.reasoningEffort === "string"
|
|
167
|
+
? profile.metadata.reasoningEffort
|
|
168
|
+
: null;
|
|
169
|
+
}
|
|
170
|
+
function readVerbosity(profile) {
|
|
171
|
+
return typeof profile.metadata.verbosity === "string"
|
|
172
|
+
? profile.metadata.verbosity
|
|
173
|
+
: null;
|
|
174
|
+
}
|
|
175
|
+
function buildReasoningConfiguration(profile) {
|
|
176
|
+
const effort = readReasoningEffort(profile);
|
|
177
|
+
return effort ? { effort } : undefined;
|
|
178
|
+
}
|
|
179
|
+
function buildTextConfiguration(options) {
|
|
180
|
+
const text = {};
|
|
181
|
+
const verbosity = readVerbosity(options.profile);
|
|
182
|
+
if (verbosity) {
|
|
183
|
+
text.verbosity = verbosity;
|
|
184
|
+
}
|
|
185
|
+
if (options.format) {
|
|
186
|
+
text.format = options.format;
|
|
187
|
+
}
|
|
188
|
+
return Object.keys(text).length > 0 ? text : undefined;
|
|
189
|
+
}
|
|
190
|
+
function estimateTokens(text) {
|
|
191
|
+
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
192
|
+
}
|
|
193
|
+
function computeSourceExcerpt(profile, sourceText) {
|
|
194
|
+
const contextWindow = MODEL_CONTEXT_WINDOWS[profile.model] ?? DEFAULT_CONTEXT_WINDOW;
|
|
195
|
+
const inputBudget = Math.max(16_000, contextWindow - RESERVED_RESPONSE_TOKENS);
|
|
196
|
+
const estimatedTokens = estimateTokens(sourceText);
|
|
197
|
+
if (estimatedTokens <= inputBudget) {
|
|
198
|
+
return {
|
|
199
|
+
sourceExcerpt: sourceText,
|
|
200
|
+
estimatedTokens,
|
|
201
|
+
contextWindow,
|
|
202
|
+
inputBudget,
|
|
203
|
+
truncated: false
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const allowedChars = Math.max(8_000, Math.floor(inputBudget * APPROX_CHARS_PER_TOKEN));
|
|
207
|
+
return {
|
|
208
|
+
sourceExcerpt: sourceText.slice(0, allowedChars),
|
|
209
|
+
estimatedTokens,
|
|
210
|
+
contextWindow,
|
|
211
|
+
inputBudget,
|
|
212
|
+
truncated: true
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async function readJsonPayload(response) {
|
|
216
|
+
const payload = (await response.json());
|
|
217
|
+
return payload;
|
|
218
|
+
}
|
|
219
|
+
function readResponseStatus(payload) {
|
|
220
|
+
return typeof payload.status === "string" ? payload.status : null;
|
|
221
|
+
}
|
|
222
|
+
function readResponseId(payload) {
|
|
223
|
+
return typeof payload.id === "string" ? payload.id : null;
|
|
224
|
+
}
|
|
225
|
+
function readResponseError(payload) {
|
|
226
|
+
const error = payload.error;
|
|
227
|
+
if (!error || typeof error !== "object") {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const message = typeof error.message === "string"
|
|
231
|
+
? error.message
|
|
232
|
+
: null;
|
|
233
|
+
return message;
|
|
234
|
+
}
|
|
235
|
+
function isTerminalBackgroundStatus(status) {
|
|
236
|
+
return (status === "completed" ||
|
|
237
|
+
status === "failed" ||
|
|
238
|
+
status === "cancelled" ||
|
|
239
|
+
status === "incomplete");
|
|
240
|
+
}
|
|
25
241
|
function normalizeResult(content, input) {
|
|
26
242
|
if (!content) {
|
|
27
243
|
return null;
|
|
@@ -31,7 +247,8 @@ function normalizeResult(content, input) {
|
|
|
31
247
|
return {
|
|
32
248
|
title: parsed.title?.trim() || input.titleHint || "Imported source",
|
|
33
249
|
summary: parsed.summary?.trim() || "",
|
|
34
|
-
markdown: parsed.markdown?.trim() ||
|
|
250
|
+
markdown: parsed.markdown?.trim() ||
|
|
251
|
+
`# ${parsed.title?.trim() || input.titleHint || "Imported source"}\n\n${parsed.summary?.trim() || "Imported source prepared for Forge review."}\n`,
|
|
35
252
|
tags: Array.isArray(parsed.tags)
|
|
36
253
|
? parsed.tags.filter((tag) => typeof tag === "string")
|
|
37
254
|
: [],
|
|
@@ -52,22 +269,201 @@ function normalizeResult(content, input) {
|
|
|
52
269
|
}
|
|
53
270
|
export class OpenAiResponsesProvider {
|
|
54
271
|
providerNames = ["openai", "openai-responses", "openai-compatible"];
|
|
55
|
-
async
|
|
272
|
+
async testConnection({ apiKey, profile, logger }) {
|
|
273
|
+
emitDiagnostic(logger, {
|
|
274
|
+
level: "info",
|
|
275
|
+
message: "Testing OpenAI wiki connection.",
|
|
276
|
+
details: {
|
|
277
|
+
scope: "wiki_llm",
|
|
278
|
+
eventKey: "llm_connection_test_start",
|
|
279
|
+
provider: profile.provider,
|
|
280
|
+
baseUrl: profile.baseUrl,
|
|
281
|
+
model: profile.model
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
let response;
|
|
285
|
+
try {
|
|
286
|
+
response = await fetch(`${profile.baseUrl.replace(/\/$/, "")}/responses`, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: {
|
|
289
|
+
"content-type": "application/json",
|
|
290
|
+
authorization: `Bearer ${apiKey}`
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify({
|
|
293
|
+
model: profile.model,
|
|
294
|
+
input: "Reply with the single word ok.",
|
|
295
|
+
max_output_tokens: 24,
|
|
296
|
+
reasoning: buildReasoningConfiguration(profile),
|
|
297
|
+
text: buildTextConfiguration({ profile })
|
|
298
|
+
})
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
emitDiagnostic(logger, {
|
|
303
|
+
level: "error",
|
|
304
|
+
message: "OpenAI connection test could not reach the provider.",
|
|
305
|
+
details: {
|
|
306
|
+
scope: "wiki_llm",
|
|
307
|
+
eventKey: "llm_connection_test_transport_error",
|
|
308
|
+
provider: profile.provider,
|
|
309
|
+
baseUrl: profile.baseUrl,
|
|
310
|
+
model: profile.model,
|
|
311
|
+
error: error instanceof Error
|
|
312
|
+
? {
|
|
313
|
+
name: error.name,
|
|
314
|
+
message: error.message,
|
|
315
|
+
stack: error.stack ?? null
|
|
316
|
+
}
|
|
317
|
+
: String(error)
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
const message = await response.text();
|
|
324
|
+
emitDiagnostic(logger, {
|
|
325
|
+
level: "error",
|
|
326
|
+
message: `OpenAI connection test failed (${response.status}).`,
|
|
327
|
+
details: {
|
|
328
|
+
scope: "wiki_llm",
|
|
329
|
+
eventKey: "llm_connection_test_failed",
|
|
330
|
+
provider: profile.provider,
|
|
331
|
+
baseUrl: profile.baseUrl,
|
|
332
|
+
model: profile.model,
|
|
333
|
+
status: response.status,
|
|
334
|
+
responseBody: truncate(message)
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
throw new Error(`OpenAI connection test failed (${response.status})${message ? `: ${message}` : ""}`);
|
|
338
|
+
}
|
|
339
|
+
const payload = await readJsonPayload(response);
|
|
340
|
+
emitDiagnostic(logger, {
|
|
341
|
+
level: "info",
|
|
342
|
+
message: "OpenAI connection test completed.",
|
|
343
|
+
details: {
|
|
344
|
+
scope: "wiki_llm",
|
|
345
|
+
eventKey: "llm_connection_test_success",
|
|
346
|
+
provider: profile.provider,
|
|
347
|
+
baseUrl: profile.baseUrl,
|
|
348
|
+
model: profile.model,
|
|
349
|
+
outputPreview: truncate(parseOutputText(payload)?.trim() || "ok", 240)
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return {
|
|
353
|
+
outputPreview: parseOutputText(payload)?.trim() || "ok"
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async compile({ apiKey, profile, input, resumeResponseId, logger }) {
|
|
357
|
+
const sourceText = input.rawText.trim();
|
|
56
358
|
const prompt = [
|
|
57
|
-
"You
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"-
|
|
62
|
-
"-
|
|
63
|
-
"-
|
|
64
|
-
"-
|
|
65
|
-
"
|
|
66
|
-
"-
|
|
359
|
+
"You convert user-provided source material into reviewable Forge wiki drafts.",
|
|
360
|
+
"You are preparing candidates for human review. Do not publish anything directly.",
|
|
361
|
+
"Return strict JSON with exactly these top-level keys: title, summary, markdown, tags, entityProposals, pageUpdateSuggestions, articleCandidates.",
|
|
362
|
+
"Goal:",
|
|
363
|
+
"- Produce one main overview page in markdown.",
|
|
364
|
+
"- Split durable subtopics into articleCandidates with full draft markdown.",
|
|
365
|
+
"- Propose Forge entities only when the source clearly supports a durable record.",
|
|
366
|
+
"- Suggest page updates only when the source clearly belongs in an existing page instead of a new page.",
|
|
367
|
+
"What Forge expects:",
|
|
368
|
+
"- The main markdown should be a readable overview or anchor page, not a raw source dump.",
|
|
369
|
+
"- articleCandidates should contain real draft wiki pages with their own markdown, not just titles.",
|
|
370
|
+
"- entityProposals must use one of these entityType values only: goal, project, task, habit, strategy, psyche_value, note.",
|
|
371
|
+
"- Use suggestedFields only for fields that truly fit the entity type. Use null for unknown scalar fields and [] for unknown list fields.",
|
|
372
|
+
"- Keep entity proposals conservative. Prefer wiki pages when the source is informative but not actionable enough for a Forge entity.",
|
|
373
|
+
"Forge ontology:",
|
|
374
|
+
"- Forge has two durable knowledge surfaces: wiki pages and structured entities.",
|
|
375
|
+
"- Use wiki pages for rich context, summaries, explanations, relationships, timelines, source synthesis, and themes that are broader than one action item.",
|
|
376
|
+
"- Use entities for operational objects that Forge can track directly: goals, projects, tasks, habits, strategies, psyche values, and durable notes.",
|
|
377
|
+
"- When the same topic needs both explanation and operations, create both: a wiki page for context and an entity proposal for the operational record.",
|
|
378
|
+
"Forge wiki writing rules:",
|
|
379
|
+
"- Use clean Markdown headings and short sections.",
|
|
380
|
+
"- Use [[wiki links]] only for durable concepts, people, projects, places, or pages that should exist in the wiki.",
|
|
381
|
+
"- Prefer factual, compressed, agent-usable writing over decorative prose.",
|
|
382
|
+
"- When the source is a chat, transcript, or message log, do not reproduce turn-by-turn conversation unless the exact wording is the durable artifact.",
|
|
383
|
+
"- For chats and transcripts, extract the durable parts: people, relationships, ongoing projects, commitments, habits, values, decisions, questions, sources, and evidence.",
|
|
384
|
+
"- Merge repetitive back-and-forth into concise summaries.",
|
|
385
|
+
"- Use short quotes only when the exact phrase matters.",
|
|
386
|
+
"How to split pages:",
|
|
387
|
+
"- Keep markdown as the overview page for this source.",
|
|
388
|
+
"- If one topic deserves its own page, put it in articleCandidates with title, slug, summary, rationale, markdown, tags, aliases, and parentSlug.",
|
|
389
|
+
"- Use parentSlug when the draft page clearly belongs under an existing Forge wiki branch such as people, projects, concepts, sources, or chronicle.",
|
|
390
|
+
"- Do not create articleCandidates for every minor mention; only create pages that would be useful to reopen later.",
|
|
391
|
+
"Useful high-level wiki themes:",
|
|
392
|
+
"- people: people, collaborators, family, teams, roles, relationship context.",
|
|
393
|
+
"- projects: bounded initiatives, active efforts, plans, milestones, workstreams.",
|
|
394
|
+
"- concepts: ideas, beliefs, frameworks, principles, definitions, recurring themes.",
|
|
395
|
+
"- sources: books, papers, links, chats, files, interviews, meetings, datasets, media.",
|
|
396
|
+
"- chronicle: dated notes, decisions, turning points, retrospectives, timelines, event sequences.",
|
|
397
|
+
"- values: valued directions, principles, motives, what matters, meaning, purpose.",
|
|
398
|
+
"- practices: routines, rituals, habits, protocols, checklists, playbooks, recipes, methods.",
|
|
399
|
+
"- health: sleep, sport, recovery, symptoms, treatments, biometrics, body-related observations.",
|
|
400
|
+
"- places: homes, cities, venues, clinics, schools, travel locations, recurring environments.",
|
|
401
|
+
"- areas: enduring life domains such as relationships, work, learning, finances, home, family, and community.",
|
|
402
|
+
"- decisions: important choices, tradeoffs, commitments, rules, criteria, and open questions.",
|
|
403
|
+
"- Use these themes as clustering lenses. They are inspired by recurring dimensions in well-being and self-reflection literature: relationships, meaning, accomplishment, growth, health, environment, and life context.",
|
|
404
|
+
"- If the source strongly fits one of these themes but does not justify a Forge entity, create a wiki page draft instead of forcing an entity.",
|
|
405
|
+
"How to propose entities:",
|
|
406
|
+
"- goal: a durable desired outcome with an explicit direction or finish line.",
|
|
407
|
+
"- project: a bounded initiative with a concrete scope, usually linked to a goal.",
|
|
408
|
+
"- task: a single actionable work item, not a broad initiative.",
|
|
409
|
+
"- habit: repeated behavior with a cadence or rule.",
|
|
410
|
+
"- strategy: a repeatable or structured approach linked to a goal, project, or task.",
|
|
411
|
+
"- psyche_value: a durable value, principle, or valued direction that explains what matters to the user.",
|
|
412
|
+
"- note: only when the source implies a durable evidence note that should exist as a record outside the wiki page itself.",
|
|
413
|
+
"Entity proposal rules:",
|
|
414
|
+
"- Never invent IDs.",
|
|
415
|
+
"- Do not propose an entity just because it is mentioned once without durable importance.",
|
|
416
|
+
"- People, concepts, sources, places, and broad life areas are usually wiki pages, not Forge entities.",
|
|
417
|
+
"- Goals should be outcomes, not chores.",
|
|
418
|
+
"- Projects should group multiple steps or phases, not a single errand.",
|
|
419
|
+
"- Tasks should be concrete and actionable enough to do soon.",
|
|
420
|
+
"- Habits should represent repeated behaviors, routines, or standing rules.",
|
|
421
|
+
"- Strategies should represent reusable plans, heuristics, protocols, or decision patterns.",
|
|
422
|
+
"- Psyche values should capture what matters and why, not merely preferences or moods.",
|
|
423
|
+
"- Notes should be evidence-like records, observations, excerpts, or source captures that deserve their own durable object.",
|
|
424
|
+
"- If a project is proposed, include a plausible goal linkage in suggestedFields when the source makes that relationship clear.",
|
|
425
|
+
"- If a habit is proposed, include polarity, frequency, linked goals/projects/tasks, and cadence details when present.",
|
|
426
|
+
"- If a psyche_value is proposed, use valuedDirection, whyItMatters, and linked goals/projects/tasks when present.",
|
|
427
|
+
"- If the source mentions an existing Forge-like object but only adds background context, prefer a pageUpdateSuggestion or wiki page rather than creating a duplicate entity.",
|
|
428
|
+
"What to capture from common source types:",
|
|
429
|
+
"- Chats and message logs: relationship facts, plans, promises, recurring activities, values, concerns, decisions, and candidate projects or habits.",
|
|
430
|
+
"- Articles, books, and notes: concepts, sources, claims, frameworks, quotes worth preserving, and related projects or values.",
|
|
431
|
+
"- Personal logs and journals: chronology, patterns, self-observations, decisions, practices, health notes, relationship dynamics, and values under tension.",
|
|
432
|
+
"- Meeting notes: decisions, owners, projects, tasks, open questions, and source evidence.",
|
|
433
|
+
"Page update rules:",
|
|
434
|
+
"- Only emit pageUpdateSuggestions when the source clearly belongs in an existing page.",
|
|
435
|
+
"- Keep patchSummary concise and specific so Forge can append it safely.",
|
|
436
|
+
"Output discipline:",
|
|
437
|
+
"- Always return every top-level key.",
|
|
438
|
+
"- Always return every nested object field required by the schema.",
|
|
439
|
+
"- Use empty arrays instead of omitting lists.",
|
|
440
|
+
"- Use null instead of omitting unknown scalar fields inside suggestedFields.",
|
|
441
|
+
"- Do not wrap the JSON in markdown fences or prose.",
|
|
67
442
|
profile.systemPrompt.trim()
|
|
68
443
|
]
|
|
69
444
|
.filter(Boolean)
|
|
70
445
|
.join("\n");
|
|
446
|
+
const sourcePlan = computeSourceExcerpt(profile, sourceText);
|
|
447
|
+
emitDiagnostic(logger, {
|
|
448
|
+
level: "info",
|
|
449
|
+
message: "Started OpenAI wiki compilation request.",
|
|
450
|
+
details: {
|
|
451
|
+
scope: "wiki_llm",
|
|
452
|
+
eventKey: "llm_compile_start",
|
|
453
|
+
provider: profile.provider,
|
|
454
|
+
baseUrl: profile.baseUrl,
|
|
455
|
+
model: profile.model,
|
|
456
|
+
mimeType: input.mimeType,
|
|
457
|
+
parseStrategy: input.parseStrategy,
|
|
458
|
+
titleHint: input.titleHint,
|
|
459
|
+
rawTextLength: input.rawText.length,
|
|
460
|
+
includesBinary: Boolean(input.binary),
|
|
461
|
+
estimatedInputTokens: sourcePlan.estimatedTokens,
|
|
462
|
+
contextWindow: sourcePlan.contextWindow,
|
|
463
|
+
inputBudget: sourcePlan.inputBudget,
|
|
464
|
+
truncated: sourcePlan.truncated
|
|
465
|
+
}
|
|
466
|
+
});
|
|
71
467
|
const inputs = [
|
|
72
468
|
{
|
|
73
469
|
role: "system",
|
|
@@ -77,13 +473,21 @@ export class OpenAiResponsesProvider {
|
|
|
77
473
|
const userContent = [
|
|
78
474
|
{
|
|
79
475
|
type: "input_text",
|
|
80
|
-
text:
|
|
476
|
+
text: [
|
|
477
|
+
`Title hint: ${input.titleHint || "none"}`,
|
|
478
|
+
`Mime type: ${input.mimeType}`,
|
|
479
|
+
`Parse strategy: ${input.parseStrategy}`,
|
|
480
|
+
`Source length: ${sourceText.length} characters`,
|
|
481
|
+
sourcePlan.truncated
|
|
482
|
+
? `Source was truncated to fit the model context budget (${sourcePlan.inputBudget} estimated input tokens). Focus on durable structure, not verbatim reproduction.`
|
|
483
|
+
: "Source was sent in full."
|
|
484
|
+
].join("\n")
|
|
81
485
|
}
|
|
82
486
|
];
|
|
83
|
-
if (
|
|
487
|
+
if (sourceText) {
|
|
84
488
|
userContent.push({
|
|
85
489
|
type: "input_text",
|
|
86
|
-
text: `Source:\n${
|
|
490
|
+
text: `Source:\n${sourcePlan.sourceExcerpt}`
|
|
87
491
|
});
|
|
88
492
|
}
|
|
89
493
|
if (input.binary &&
|
|
@@ -98,63 +502,272 @@ export class OpenAiResponsesProvider {
|
|
|
98
502
|
role: "user",
|
|
99
503
|
content: userContent
|
|
100
504
|
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
505
|
+
let payload;
|
|
506
|
+
let responseId = resumeResponseId?.trim() || null;
|
|
507
|
+
if (responseId) {
|
|
508
|
+
emitDiagnostic(logger, {
|
|
509
|
+
level: "info",
|
|
510
|
+
message: "Resuming OpenAI wiki compilation from an existing background response.",
|
|
511
|
+
details: {
|
|
512
|
+
scope: "wiki_llm",
|
|
513
|
+
eventKey: "llm_compile_background_resuming",
|
|
514
|
+
provider: profile.provider,
|
|
515
|
+
baseUrl: profile.baseUrl,
|
|
516
|
+
model: profile.model,
|
|
517
|
+
responseId
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
const resumeResponse = await fetch(`${profile.baseUrl.replace(/\/$/, "")}/responses/${responseId}`, {
|
|
521
|
+
method: "GET",
|
|
522
|
+
headers: {
|
|
523
|
+
authorization: `Bearer ${apiKey}`
|
|
524
|
+
},
|
|
525
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
526
|
+
});
|
|
527
|
+
if (!resumeResponse.ok) {
|
|
528
|
+
const message = await resumeResponse.text();
|
|
529
|
+
throw new Error(`OpenAI background wiki compilation resume failed: ${resumeResponse.status}${message ? `: ${message}` : ""}`);
|
|
530
|
+
}
|
|
531
|
+
payload = await readJsonPayload(resumeResponse);
|
|
532
|
+
emitDiagnostic(logger, {
|
|
533
|
+
level: "info",
|
|
534
|
+
message: "Forge reattached to the existing OpenAI background wiki compilation job.",
|
|
535
|
+
details: {
|
|
536
|
+
scope: "wiki_llm",
|
|
537
|
+
eventKey: "llm_compile_background_started",
|
|
538
|
+
provider: profile.provider,
|
|
539
|
+
baseUrl: profile.baseUrl,
|
|
540
|
+
model: profile.model,
|
|
541
|
+
responseId,
|
|
542
|
+
status: readResponseStatus(payload),
|
|
543
|
+
resumed: true
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
let createResponse;
|
|
549
|
+
try {
|
|
550
|
+
createResponse = await fetch(`${profile.baseUrl.replace(/\/$/, "")}/responses`, {
|
|
551
|
+
method: "POST",
|
|
552
|
+
headers: {
|
|
553
|
+
"content-type": "application/json",
|
|
554
|
+
authorization: `Bearer ${apiKey}`
|
|
555
|
+
},
|
|
556
|
+
body: JSON.stringify({
|
|
557
|
+
model: profile.model,
|
|
558
|
+
input: inputs,
|
|
559
|
+
store: true,
|
|
560
|
+
background: true,
|
|
561
|
+
prompt_cache_retention: profile.model === "gpt-5.4" ? "24h" : "in_memory",
|
|
562
|
+
prompt_cache_key: `forge-wiki-ingest:${profile.model}:${input.parseStrategy}:${input.mimeType}`,
|
|
563
|
+
reasoning: buildReasoningConfiguration(profile),
|
|
564
|
+
text: buildTextConfiguration({
|
|
565
|
+
profile,
|
|
566
|
+
format: {
|
|
567
|
+
type: "json_schema",
|
|
568
|
+
name: "forge_wiki_ingest_compilation",
|
|
569
|
+
strict: true,
|
|
570
|
+
schema: buildWikiIngestSchema()
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
}),
|
|
574
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
const finalError = error instanceof Error ? error : new Error(String(error));
|
|
579
|
+
emitDiagnostic(logger, {
|
|
580
|
+
level: "error",
|
|
581
|
+
message: "OpenAI wiki compilation could not reach the provider.",
|
|
582
|
+
details: {
|
|
583
|
+
scope: "wiki_llm",
|
|
584
|
+
eventKey: "llm_compile_transport_error",
|
|
585
|
+
provider: profile.provider,
|
|
586
|
+
baseUrl: profile.baseUrl,
|
|
587
|
+
model: profile.model,
|
|
588
|
+
mimeType: input.mimeType,
|
|
589
|
+
parseStrategy: input.parseStrategy,
|
|
590
|
+
rawTextLength: input.rawText.length,
|
|
591
|
+
estimatedInputTokens: sourcePlan.estimatedTokens,
|
|
592
|
+
truncated: sourcePlan.truncated,
|
|
593
|
+
error: {
|
|
594
|
+
name: finalError.name,
|
|
595
|
+
message: finalError.message,
|
|
596
|
+
stack: finalError.stack ?? null
|
|
149
597
|
}
|
|
150
598
|
}
|
|
599
|
+
});
|
|
600
|
+
throw finalError;
|
|
601
|
+
}
|
|
602
|
+
if (!createResponse.ok) {
|
|
603
|
+
const message = await createResponse.text();
|
|
604
|
+
emitDiagnostic(logger, {
|
|
605
|
+
level: "error",
|
|
606
|
+
message: `LLM compilation failed: ${createResponse.status}`,
|
|
607
|
+
details: {
|
|
608
|
+
scope: "wiki_llm",
|
|
609
|
+
eventKey: "llm_compile_failed",
|
|
610
|
+
provider: profile.provider,
|
|
611
|
+
baseUrl: profile.baseUrl,
|
|
612
|
+
model: profile.model,
|
|
613
|
+
mimeType: input.mimeType,
|
|
614
|
+
parseStrategy: input.parseStrategy,
|
|
615
|
+
rawTextLength: input.rawText.length,
|
|
616
|
+
estimatedInputTokens: sourcePlan.estimatedTokens,
|
|
617
|
+
truncated: sourcePlan.truncated,
|
|
618
|
+
status: createResponse.status,
|
|
619
|
+
responseBody: truncate(message)
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
throw new Error(`LLM compilation failed: ${createResponse.status}${message ? `: ${message}` : ""}`);
|
|
623
|
+
}
|
|
624
|
+
payload = await readJsonPayload(createResponse);
|
|
625
|
+
responseId = readResponseId(payload);
|
|
626
|
+
if (!responseId) {
|
|
627
|
+
throw new Error("OpenAI background response did not include an id for polling.");
|
|
628
|
+
}
|
|
629
|
+
emitDiagnostic(logger, {
|
|
630
|
+
level: "info",
|
|
631
|
+
message: "OpenAI accepted the wiki compilation job for background processing.",
|
|
632
|
+
details: {
|
|
633
|
+
scope: "wiki_llm",
|
|
634
|
+
eventKey: "llm_compile_background_started",
|
|
635
|
+
provider: profile.provider,
|
|
636
|
+
baseUrl: profile.baseUrl,
|
|
637
|
+
model: profile.model,
|
|
638
|
+
responseId,
|
|
639
|
+
status: readResponseStatus(payload),
|
|
640
|
+
resumed: false
|
|
151
641
|
}
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
let pollCount = 0;
|
|
645
|
+
let consecutivePollFailures = 0;
|
|
646
|
+
while (!isTerminalBackgroundStatus(readResponseStatus(payload))) {
|
|
647
|
+
await new Promise((resolve) => setTimeout(resolve, BACKGROUND_POLL_INTERVAL_MS));
|
|
648
|
+
try {
|
|
649
|
+
const pollResponse = await fetch(`${profile.baseUrl.replace(/\/$/, "")}/responses/${responseId}`, {
|
|
650
|
+
method: "GET",
|
|
651
|
+
headers: {
|
|
652
|
+
authorization: `Bearer ${apiKey}`
|
|
653
|
+
},
|
|
654
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
655
|
+
});
|
|
656
|
+
if (!pollResponse.ok) {
|
|
657
|
+
const message = await pollResponse.text();
|
|
658
|
+
if (pollResponse.status >= 500) {
|
|
659
|
+
consecutivePollFailures += 1;
|
|
660
|
+
emitDiagnostic(logger, {
|
|
661
|
+
level: "warning",
|
|
662
|
+
message: "OpenAI background wiki compilation polling hit a server error. Forge will keep retrying.",
|
|
663
|
+
details: {
|
|
664
|
+
scope: "wiki_llm",
|
|
665
|
+
eventKey: "llm_compile_background_poll_retry",
|
|
666
|
+
provider: profile.provider,
|
|
667
|
+
baseUrl: profile.baseUrl,
|
|
668
|
+
model: profile.model,
|
|
669
|
+
responseId,
|
|
670
|
+
pollCount,
|
|
671
|
+
consecutivePollFailures,
|
|
672
|
+
status: pollResponse.status,
|
|
673
|
+
responseBody: truncate(message)
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
throw new Error(`OpenAI background wiki compilation polling failed: ${pollResponse.status}${message ? `: ${message}` : ""}`);
|
|
679
|
+
}
|
|
680
|
+
payload = await readJsonPayload(pollResponse);
|
|
681
|
+
pollCount += 1;
|
|
682
|
+
consecutivePollFailures = 0;
|
|
683
|
+
emitDiagnostic(logger, {
|
|
684
|
+
level: "info",
|
|
685
|
+
message: "Polled OpenAI background wiki compilation status.",
|
|
686
|
+
details: {
|
|
687
|
+
scope: "wiki_llm",
|
|
688
|
+
eventKey: "llm_compile_background_polled",
|
|
689
|
+
provider: profile.provider,
|
|
690
|
+
baseUrl: profile.baseUrl,
|
|
691
|
+
model: profile.model,
|
|
692
|
+
responseId,
|
|
693
|
+
pollCount,
|
|
694
|
+
status: readResponseStatus(payload)
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
const finalError = error instanceof Error ? error : new Error(String(error));
|
|
700
|
+
const isRetriableTransport = finalError.name === "TypeError" ||
|
|
701
|
+
finalError.name === "TimeoutError" ||
|
|
702
|
+
/fetch failed/i.test(finalError.message) ||
|
|
703
|
+
/network/i.test(finalError.message) ||
|
|
704
|
+
/timeout/i.test(finalError.message);
|
|
705
|
+
if (!isRetriableTransport) {
|
|
706
|
+
throw finalError;
|
|
707
|
+
}
|
|
708
|
+
consecutivePollFailures += 1;
|
|
709
|
+
emitDiagnostic(logger, {
|
|
710
|
+
level: "warning",
|
|
711
|
+
message: "OpenAI background wiki compilation polling lost connectivity. Forge will keep retrying.",
|
|
712
|
+
details: {
|
|
713
|
+
scope: "wiki_llm",
|
|
714
|
+
eventKey: "llm_compile_background_poll_retry",
|
|
715
|
+
provider: profile.provider,
|
|
716
|
+
baseUrl: profile.baseUrl,
|
|
717
|
+
model: profile.model,
|
|
718
|
+
responseId,
|
|
719
|
+
pollCount,
|
|
720
|
+
consecutivePollFailures,
|
|
721
|
+
error: {
|
|
722
|
+
name: finalError.name,
|
|
723
|
+
message: finalError.message,
|
|
724
|
+
stack: finalError.stack ?? null
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const finalStatus = readResponseStatus(payload);
|
|
731
|
+
if (finalStatus !== "completed") {
|
|
732
|
+
const errorMessage = readResponseError(payload) ??
|
|
733
|
+
`OpenAI background wiki compilation ended with status ${finalStatus}.`;
|
|
734
|
+
emitDiagnostic(logger, {
|
|
735
|
+
level: "error",
|
|
736
|
+
message: errorMessage,
|
|
737
|
+
details: {
|
|
738
|
+
scope: "wiki_llm",
|
|
739
|
+
eventKey: "llm_compile_background_terminal_error",
|
|
740
|
+
provider: profile.provider,
|
|
741
|
+
baseUrl: profile.baseUrl,
|
|
742
|
+
model: profile.model,
|
|
743
|
+
responseId,
|
|
744
|
+
status: finalStatus
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
throw new Error(errorMessage);
|
|
156
748
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
749
|
+
const content = parseOutputText(payload);
|
|
750
|
+
const result = normalizeResult(content, input);
|
|
751
|
+
emitDiagnostic(logger, {
|
|
752
|
+
level: result ? "info" : "warning",
|
|
753
|
+
message: result
|
|
754
|
+
? "OpenAI wiki compilation returned structured output."
|
|
755
|
+
: "OpenAI wiki compilation returned output that could not be normalized.",
|
|
756
|
+
details: {
|
|
757
|
+
scope: "wiki_llm",
|
|
758
|
+
eventKey: result ? "llm_compile_success" : "llm_compile_unparseable",
|
|
759
|
+
provider: profile.provider,
|
|
760
|
+
baseUrl: profile.baseUrl,
|
|
761
|
+
model: profile.model,
|
|
762
|
+
responseId,
|
|
763
|
+
estimatedInputTokens: sourcePlan.estimatedTokens,
|
|
764
|
+
truncated: sourcePlan.truncated,
|
|
765
|
+
responsePreview: truncate(content ?? "", 600),
|
|
766
|
+
articleCandidateCount: result?.articleCandidates.length ?? 0,
|
|
767
|
+
entityProposalCount: result?.entityProposals.length ?? 0,
|
|
768
|
+
pageUpdateSuggestionCount: result?.pageUpdateSuggestions.length ?? 0
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
return result;
|
|
159
772
|
}
|
|
160
773
|
}
|