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.
Files changed (33) hide show
  1. package/dist/assets/{board-DGbXWEuu.js → board-_C6oMy5w.js} +2 -2
  2. package/dist/assets/{board-DGbXWEuu.js.map → board-_C6oMy5w.js.map} +1 -1
  3. package/dist/assets/index-Ch_xeZ2u.js +63 -0
  4. package/dist/assets/index-Ch_xeZ2u.js.map +1 -0
  5. package/dist/assets/index-DvVM7K6j.css +1 -0
  6. package/dist/assets/{motion-B5Qoz2Ci.js → motion-D4sZgCHd.js} +2 -2
  7. package/dist/assets/{motion-B5Qoz2Ci.js.map → motion-D4sZgCHd.js.map} +1 -1
  8. package/dist/assets/{table-D_iurDQu.js → table-BWzTaky1.js} +2 -2
  9. package/dist/assets/{table-D_iurDQu.js.map → table-BWzTaky1.js.map} +1 -1
  10. package/dist/assets/{ui-D5QUYUq4.js → ui-BzK4azQb.js} +2 -2
  11. package/dist/assets/{ui-D5QUYUq4.js.map → ui-BzK4azQb.js.map} +1 -1
  12. package/dist/assets/vendor-De38P6YR.js +729 -0
  13. package/dist/assets/vendor-De38P6YR.js.map +1 -0
  14. package/dist/assets/{viz-BD9WSxHz.js → viz-C6hfyqzu.js} +2 -2
  15. package/dist/assets/{viz-BD9WSxHz.js.map → viz-C6hfyqzu.js.map} +1 -1
  16. package/dist/index.html +8 -8
  17. package/dist/server/app.js +328 -19
  18. package/dist/server/health.js +82 -21
  19. package/dist/server/managers/platform/background-job-manager.js +103 -8
  20. package/dist/server/managers/platform/llm-manager.js +91 -5
  21. package/dist/server/managers/platform/openai-responses-provider.js +683 -70
  22. package/dist/server/repositories/diagnostic-logs.js +243 -0
  23. package/dist/server/repositories/wiki-memory.js +619 -66
  24. package/dist/server/types.js +56 -0
  25. package/openclaw.plugin.json +1 -1
  26. package/package.json +1 -1
  27. package/server/migrations/023_diagnostic_logs.sql +28 -0
  28. package/skills/forge-openclaw/SKILL.md +14 -0
  29. package/dist/assets/index-4-1WI9i7.css +0 -1
  30. package/dist/assets/index-BZbHajNK.js +0 -63
  31. package/dist/assets/index-BZbHajNK.js.map +0 -1
  32. package/dist/assets/vendor-KARp8LAR.js +0 -706
  33. package/dist/assets/vendor-KARp8LAR.js.map +0 -1
package/dist/index.html CHANGED
@@ -13,15 +13,15 @@
13
13
  />
14
14
  <link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
15
15
  <link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
16
- <script type="module" crossorigin src="/forge/assets/index-BZbHajNK.js"></script>
17
- <link rel="modulepreload" crossorigin href="/forge/assets/viz-BD9WSxHz.js">
18
- <link rel="modulepreload" crossorigin href="/forge/assets/vendor-KARp8LAR.js">
19
- <link rel="modulepreload" crossorigin href="/forge/assets/ui-D5QUYUq4.js">
20
- <link rel="modulepreload" crossorigin href="/forge/assets/motion-B5Qoz2Ci.js">
21
- <link rel="modulepreload" crossorigin href="/forge/assets/table-D_iurDQu.js">
22
- <link rel="modulepreload" crossorigin href="/forge/assets/board-DGbXWEuu.js">
16
+ <script type="module" crossorigin src="/forge/assets/index-Ch_xeZ2u.js"></script>
17
+ <link rel="modulepreload" crossorigin href="/forge/assets/viz-C6hfyqzu.js">
18
+ <link rel="modulepreload" crossorigin href="/forge/assets/vendor-De38P6YR.js">
19
+ <link rel="modulepreload" crossorigin href="/forge/assets/ui-BzK4azQb.js">
20
+ <link rel="modulepreload" crossorigin href="/forge/assets/motion-D4sZgCHd.js">
21
+ <link rel="modulepreload" crossorigin href="/forge/assets/table-BWzTaky1.js">
22
+ <link rel="modulepreload" crossorigin href="/forge/assets/board-_C6oMy5w.js">
23
23
  <link rel="stylesheet" crossorigin href="/forge/assets/vendor-DT3pnAKJ.css">
24
- <link rel="stylesheet" crossorigin href="/forge/assets/index-4-1WI9i7.css">
24
+ <link rel="stylesheet" crossorigin href="/forge/assets/index-DvVM7K6j.css">
25
25
  </head>
26
26
  <body class="bg-canvas text-ink antialiased">
27
27
  <div id="root"></div>
@@ -7,11 +7,12 @@ import { HttpError, isHttpError } from "./errors.js";
7
7
  import { listActivityEvents, listActivityEventsForTask, recordActivityEvent, removeActivityEvent } from "./repositories/activity-events.js";
8
8
  import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
9
9
  import { listEventLog } from "./repositories/event-log.js";
10
+ import { createDiagnosticMessage, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS, enforceDiagnosticLogRetention, listDiagnosticLogs, normalizeDiagnosticSource, recordDiagnosticLog, serializeDiagnosticError } from "./repositories/diagnostic-logs.js";
10
11
  import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
11
12
  import { createHabit, createHabitCheckIn, deleteHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
12
13
  import { listDomains } from "./repositories/domains.js";
13
14
  import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
14
- import { createWikiIngestJobSchema, createUploadedWikiIngestJob, createWikiSpace, createWikiSpaceSchema, deleteWikiProfile, getWikiHealth, getWikiIngestJob, getWikiHomePageDetail, getWikiPageDetail, getWikiPageDetailBySlug, getWikiSettingsPayload, ingestWikiSource, listWikiIngestJobs, listWikiPageTree, listWikiPages, listWikiSpaces, processWikiIngestJob, reindexWikiEmbeddings, reindexWikiEmbeddingsSchema, reviewWikiIngestJob, reviewWikiIngestJobSchema, searchWikiPages, syncWikiVaultFromDisk, syncWikiVaultSchema, upsertWikiEmbeddingProfile, upsertWikiEmbeddingProfileSchema, upsertWikiLlmProfile, upsertWikiLlmProfileSchema, wikiSearchQuerySchema } from "./repositories/wiki-memory.js";
15
+ import { createWikiIngestJobSchema, createUploadedWikiIngestJob, createWikiSpace, createWikiSpaceSchema, deleteWikiIngestJob, deleteWikiProfile, getWikiHealth, getWikiIngestJob, getWikiHomePageDetail, getWikiPageDetail, getWikiPageDetailBySlug, getWikiSettingsPayload, ingestWikiSource, listWikiIngestJobs, listWikiLlmProfiles, listWikiPageTree, listWikiPages, listWikiSpaces, processWikiIngestJob, reindexWikiEmbeddings, reindexWikiEmbeddingsSchema, rerunWikiIngestJob, reviewWikiIngestJob, reviewWikiIngestJobSchema, searchWikiPages, syncWikiVaultFromDisk, syncWikiVaultSchema, testWikiLlmProfileSchema, upsertWikiEmbeddingProfile, upsertWikiEmbeddingProfileSchema, upsertWikiLlmProfile, upsertWikiLlmProfileSchema, wikiSearchQuerySchema } from "./repositories/wiki-memory.js";
15
16
  import { filterOwnedEntities, setEntityOwner } from "./repositories/entity-ownership.js";
16
17
  import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "./repositories/psyche.js";
17
18
  import { createProject, updateProject } from "./repositories/projects.js";
@@ -39,12 +40,12 @@ import { suggestTags } from "./services/tagging.js";
39
40
  import { CalendarConnectionConflictError, completeMicrosoftCalendarOauth, createCalendarConnection, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
40
41
  import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
41
42
  import { createPreferenceCatalogItemSchema, createPreferenceCatalogSchema, createPreferenceContextSchema, createPreferenceItemSchema, enqueueEntityPreferenceItemSchema, mergePreferenceContextsSchema, preferenceWorkspaceQuerySchema, startPreferenceGameSchema, submitAbsoluteSignalSchema, submitPairwiseJudgmentSchema, updatePreferenceCatalogItemSchema, updatePreferenceCatalogSchema, updatePreferenceContextSchema, updatePreferenceItemSchema, updatePreferenceScoreSchema } from "./preferences-types.js";
42
- import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
43
+ import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, createDiagnosticLogSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, diagnosticLogListQuerySchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
43
44
  import { buildOpenApiDocument } from "./openapi.js";
44
45
  import { registerWebRoutes } from "./web.js";
45
46
  import { createManagerRuntime } from "./managers/runtime.js";
46
47
  import { isManagerError } from "./managers/type-guards.js";
47
- import { createCompanionPairingSession, createCompanionPairingSessionSchema, getCompanionOverview, getFitnessViewData, getSleepViewData, ingestMobileHealthSync, mobileHealthSyncSchema, revokeCompanionPairingSession, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
48
+ import { createCompanionPairingSession, createCompanionPairingSessionSchema, getCompanionOverview, getFitnessViewData, getSleepViewData, ingestMobileHealthSync, mobileHealthSyncSchema, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
48
49
  const COMPATIBILITY_SUNSET = "transitional-node";
49
50
  function markCompatibilityRoute(reply) {
50
51
  reply.header("Deprecation", "true");
@@ -3517,11 +3518,93 @@ export async function buildServer(options = {}) {
3517
3518
  credentials: true
3518
3519
  });
3519
3520
  await app.register(multipart);
3521
+ enforceDiagnosticLogRetention({ force: true });
3522
+ const diagnosticRetentionTimer = setInterval(() => {
3523
+ try {
3524
+ enforceDiagnosticLogRetention({ force: true });
3525
+ }
3526
+ catch {
3527
+ // Diagnostics cleanup should never bring down the server loop.
3528
+ }
3529
+ }, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS);
3530
+ diagnosticRetentionTimer.unref?.();
3520
3531
  app.addHook("onClose", async () => {
3532
+ clearInterval(diagnosticRetentionTimer);
3521
3533
  taskRunWatchdog?.stop();
3522
3534
  await managers.backgroundJobs.stop();
3523
3535
  });
3524
- app.setErrorHandler((error, _request, reply) => {
3536
+ const enqueueWikiIngestJob = (jobId) => {
3537
+ managers.backgroundJobs.enqueue({
3538
+ id: jobId,
3539
+ label: `Wiki ingest ${jobId}`,
3540
+ handler: async () => {
3541
+ await processWikiIngestJob(jobId, { llm: managers.llm });
3542
+ }
3543
+ });
3544
+ };
3545
+ for (const pendingJob of listWikiIngestJobs({ limit: 100 })) {
3546
+ if (["queued", "processing"].includes(pendingJob.job.status)) {
3547
+ enqueueWikiIngestJob(pendingJob.job.id);
3548
+ }
3549
+ }
3550
+ const shouldSkipAutomaticDiagnosticRoute = (url) => {
3551
+ if (!url) {
3552
+ return false;
3553
+ }
3554
+ return (url.startsWith("/api/v1/diagnostics/logs") ||
3555
+ url === "/api/health" ||
3556
+ url === "/api/v1/health" ||
3557
+ url.startsWith("/api/v1/events/meta"));
3558
+ };
3559
+ app.addHook("onRequest", async (request) => {
3560
+ request.diagnosticStartedAt =
3561
+ process.hrtime.bigint();
3562
+ });
3563
+ app.addHook("onResponse", async (request, reply) => {
3564
+ const routeUrl = request.routeOptions.url || request.url;
3565
+ if (shouldSkipAutomaticDiagnosticRoute(routeUrl)) {
3566
+ return;
3567
+ }
3568
+ const startedAt = request.diagnosticStartedAt;
3569
+ const durationMs = typeof startedAt === "bigint"
3570
+ ? Number(process.hrtime.bigint() - startedAt) / 1_000_000
3571
+ : null;
3572
+ const source = normalizeDiagnosticSource(request.headers["x-forge-source"]);
3573
+ try {
3574
+ recordDiagnosticLog({
3575
+ level: reply.statusCode >= 500
3576
+ ? "error"
3577
+ : reply.statusCode >= 400
3578
+ ? "warning"
3579
+ : "debug",
3580
+ source,
3581
+ scope: "api_request",
3582
+ eventKey: `http_${request.method.toLowerCase()}`,
3583
+ message: createDiagnosticMessage({
3584
+ method: request.method,
3585
+ route: routeUrl,
3586
+ statusCode: reply.statusCode
3587
+ }),
3588
+ route: routeUrl,
3589
+ requestId: request.id,
3590
+ details: {
3591
+ method: request.method,
3592
+ rawUrl: request.url,
3593
+ statusCode: reply.statusCode,
3594
+ durationMs: typeof durationMs === "number"
3595
+ ? Number(durationMs.toFixed(2))
3596
+ : null,
3597
+ userAgent: typeof request.headers["user-agent"] === "string"
3598
+ ? request.headers["user-agent"]
3599
+ : null
3600
+ }
3601
+ });
3602
+ }
3603
+ catch {
3604
+ // Avoid surfacing diagnostics failures as request failures.
3605
+ }
3606
+ });
3607
+ app.setErrorHandler((error, request, reply) => {
3525
3608
  const validationIssues = error instanceof ZodError ? formatValidationIssues(error) : undefined;
3526
3609
  const statusCode = isHttpError(error)
3527
3610
  ? error.statusCode
@@ -3530,6 +3613,38 @@ export async function buildServer(options = {}) {
3530
3613
  : error instanceof ZodError
3531
3614
  ? 400
3532
3615
  : 500;
3616
+ const routeUrl = request.routeOptions.url || request.url;
3617
+ if (!shouldSkipAutomaticDiagnosticRoute(routeUrl)) {
3618
+ try {
3619
+ recordDiagnosticLog({
3620
+ level: statusCode >= 500 ? "error" : "warning",
3621
+ source: normalizeDiagnosticSource(request.headers["x-forge-source"]),
3622
+ scope: "api_error",
3623
+ eventKey: isHttpError(error)
3624
+ ? error.code
3625
+ : isManagerError(error)
3626
+ ? error.code
3627
+ : statusCode === 400
3628
+ ? "invalid_request"
3629
+ : "internal_error",
3630
+ message: getErrorMessage(error),
3631
+ route: routeUrl,
3632
+ functionName: "setErrorHandler",
3633
+ requestId: request.id,
3634
+ details: {
3635
+ statusCode,
3636
+ validationIssues: validationIssues?.map((issue) => ({
3637
+ path: issue.path,
3638
+ message: issue.message
3639
+ })) ?? [],
3640
+ error: serializeDiagnosticError(error)
3641
+ }
3642
+ });
3643
+ }
3644
+ catch {
3645
+ // Avoid cascading on the error path.
3646
+ }
3647
+ }
3533
3648
  reply.code(statusCode).send({
3534
3649
  code: isHttpError(error)
3535
3650
  ? error.code
@@ -3768,6 +3883,15 @@ export async function buildServer(options = {}) {
3768
3883
  }
3769
3884
  return { session };
3770
3885
  });
3886
+ app.post("/api/v1/health/pairing-sessions/revoke-all", async (request) => {
3887
+ const auth = requireOperatorSession(request.headers, {
3888
+ route: "/api/v1/health/pairing-sessions/revoke-all"
3889
+ });
3890
+ return revokeAllCompanionPairingSessions(revokeAllCompanionPairingSessionsSchema.parse(request.body ?? {}), {
3891
+ actor: auth.actor ?? null,
3892
+ source: "ui"
3893
+ });
3894
+ });
3771
3895
  app.post("/api/v1/mobile/pairing/verify", async (request) => ({
3772
3896
  pairing: verifyCompanionPairing(verifyCompanionPairingSchema.parse(request.body ?? {}))
3773
3897
  }));
@@ -4420,6 +4544,43 @@ export async function buildServer(options = {}) {
4420
4544
  reply.code(201);
4421
4545
  return { profile };
4422
4546
  });
4547
+ app.post("/api/v1/wiki/settings/llm-profiles/test", async (request, reply) => {
4548
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/settings/llm-profiles/test" });
4549
+ const parsed = testWikiLlmProfileSchema.parse(request.body ?? {});
4550
+ const existingProfile = parsed.profileId
4551
+ ? (listWikiLlmProfiles().find((entry) => entry.id === parsed.profileId) ?? null)
4552
+ : null;
4553
+ const profile = {
4554
+ provider: parsed.provider,
4555
+ baseUrl: parsed.baseUrl,
4556
+ model: parsed.model,
4557
+ systemPrompt: existingProfile?.systemPrompt ?? "",
4558
+ secretId: existingProfile?.secretId ?? null,
4559
+ metadata: {
4560
+ ...(existingProfile?.metadata ?? {}),
4561
+ ...(parsed.reasoningEffort
4562
+ ? { reasoningEffort: parsed.reasoningEffort }
4563
+ : {}),
4564
+ ...(parsed.verbosity ? { verbosity: parsed.verbosity } : {})
4565
+ }
4566
+ };
4567
+ const result = await managers.llm.testWikiConnection(profile, parsed.apiKey ?? null, ({ level, message, details = {} }) => {
4568
+ recordDiagnosticLog({
4569
+ level,
4570
+ source: normalizeDiagnosticSource(request.headers["x-forge-source"]),
4571
+ scope: typeof details.scope === "string" ? details.scope : "wiki_llm",
4572
+ eventKey: typeof details.eventKey === "string"
4573
+ ? details.eventKey
4574
+ : "llm_connection_test",
4575
+ message,
4576
+ route: "/api/v1/wiki/settings/llm-profiles/test",
4577
+ functionName: "testWikiConnection",
4578
+ details
4579
+ });
4580
+ });
4581
+ reply.code(200);
4582
+ return { result };
4583
+ });
4423
4584
  app.post("/api/v1/wiki/settings/embedding-profiles", async (request, reply) => {
4424
4585
  requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/settings/embedding-profiles" });
4425
4586
  const profile = upsertWikiEmbeddingProfile(upsertWikiEmbeddingProfileSchema.parse(request.body ?? {}), managers.secrets);
@@ -4527,6 +4688,33 @@ export async function buildServer(options = {}) {
4527
4688
  }
4528
4689
  return getWikiPageDetail(note.id);
4529
4690
  });
4691
+ app.delete("/api/v1/wiki/pages/:id", async (request, reply) => {
4692
+ const { id } = request.params;
4693
+ const current = getNoteById(id);
4694
+ if (!current || (current.kind !== "wiki" && current.kind !== "evidence")) {
4695
+ reply.code(404);
4696
+ return { error: "Wiki page not found" };
4697
+ }
4698
+ if (current.slug === "index") {
4699
+ reply.code(400);
4700
+ return { error: "The wiki home page cannot be deleted." };
4701
+ }
4702
+ const linkedEntityType = current.links[0]?.entityType ?? null;
4703
+ const auth = requireNoteAccess(request.headers, linkedEntityType, {
4704
+ route: "/api/v1/wiki/pages/:id",
4705
+ entityType: linkedEntityType
4706
+ });
4707
+ const deleted = deleteEntity("note", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
4708
+ if (!deleted) {
4709
+ reply.code(404);
4710
+ return { error: "Wiki page not found" };
4711
+ }
4712
+ return {
4713
+ deleted: {
4714
+ id: deleted.id
4715
+ }
4716
+ };
4717
+ });
4530
4718
  app.post("/api/v1/wiki/search", async (request) => {
4531
4719
  requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/search" });
4532
4720
  return searchWikiPages(wikiSearchQuerySchema.parse(request.body ?? {}), managers.secrets);
@@ -4553,6 +4741,28 @@ export async function buildServer(options = {}) {
4553
4741
  const readStringArrayField = (record, key) => Array.isArray(record[key])
4554
4742
  ? record[key].filter((entry) => typeof entry === "string" && entry.trim().length > 0)
4555
4743
  : [];
4744
+ const resolveMappedIngestEntity = (entityType, entityId) => {
4745
+ const result = searchEntities({
4746
+ searches: [
4747
+ {
4748
+ entityTypes: [entityType],
4749
+ ids: [entityId],
4750
+ includeDeleted: false,
4751
+ limit: 1
4752
+ }
4753
+ ]
4754
+ }).results[0];
4755
+ if (!result?.ok) {
4756
+ return null;
4757
+ }
4758
+ const match = result.matches?.find((entry) => entry.entityType === entityType && entry.id === entityId);
4759
+ return match
4760
+ ? {
4761
+ entityType,
4762
+ entityId
4763
+ }
4764
+ : null;
4765
+ };
4556
4766
  const publishIngestProposalEntity = (proposal, auth) => {
4557
4767
  const suggestedFields = proposal.suggestedFields &&
4558
4768
  typeof proposal.suggestedFields === "object" &&
@@ -4696,6 +4906,22 @@ export async function buildServer(options = {}) {
4696
4906
  }, toActivityContext(auth));
4697
4907
  return { entityType: "habit", entityId: habit.id };
4698
4908
  }
4909
+ case "psyche_value": {
4910
+ const value = createPsycheValue({
4911
+ title,
4912
+ description: summary,
4913
+ valuedDirection: readStringField(suggestedFields, "valuedDirection"),
4914
+ whyItMatters: readStringField(suggestedFields, "whyItMatters"),
4915
+ linkedGoalIds: readStringArrayField(suggestedFields, "linkedGoalIds"),
4916
+ linkedProjectIds: readStringArrayField(suggestedFields, "linkedProjectIds"),
4917
+ linkedTaskIds: readStringArrayField(suggestedFields, "linkedTaskIds"),
4918
+ committedActions: readStringArrayField(suggestedFields, "committedActions"),
4919
+ userId: typeof suggestedFields.userId === "string"
4920
+ ? suggestedFields.userId
4921
+ : null
4922
+ }, toActivityContext(auth));
4923
+ return { entityType: "psyche_value", entityId: value.id };
4924
+ }
4699
4925
  case "strategy": {
4700
4926
  const targetProjectIds = readStringArrayField(suggestedFields, "targetProjectIds");
4701
4927
  const linkedEntities = Array.isArray(suggestedFields.linkedEntities) &&
@@ -4826,13 +5052,7 @@ export async function buildServer(options = {}) {
4826
5052
  });
4827
5053
  const jobId = result.job?.job.id;
4828
5054
  if (jobId) {
4829
- managers.backgroundJobs.enqueue({
4830
- id: jobId,
4831
- label: `Wiki ingest ${jobId}`,
4832
- handler: async () => {
4833
- await processWikiIngestJob(jobId, { llm: managers.llm });
4834
- }
4835
- });
5055
+ enqueueWikiIngestJob(jobId);
4836
5056
  }
4837
5057
  reply.code(201);
4838
5058
  return result;
@@ -4849,13 +5069,7 @@ export async function buildServer(options = {}) {
4849
5069
  });
4850
5070
  const jobId = result.job?.job.id;
4851
5071
  if (jobId) {
4852
- managers.backgroundJobs.enqueue({
4853
- id: jobId,
4854
- label: `Wiki ingest ${jobId}`,
4855
- handler: async () => {
4856
- await processWikiIngestJob(jobId, { llm: managers.llm });
4857
- }
4858
- });
5072
+ enqueueWikiIngestJob(jobId);
4859
5073
  }
4860
5074
  reply.code(201);
4861
5075
  return result;
@@ -4870,13 +5084,91 @@ export async function buildServer(options = {}) {
4870
5084
  }
4871
5085
  return job;
4872
5086
  });
5087
+ app.post("/api/v1/wiki/ingest-jobs/:id/rerun", async (request, reply) => {
5088
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/rerun" });
5089
+ const { id } = request.params;
5090
+ try {
5091
+ const result = await rerunWikiIngestJob(id, {
5092
+ actor: requireAuthenticatedActor(request.headers, { route: "/api/v1/wiki/ingest-jobs/:id/rerun" }).actor ?? null
5093
+ });
5094
+ if (!result) {
5095
+ reply.code(404);
5096
+ return { error: "Wiki ingest job not found" };
5097
+ }
5098
+ const nextJobId = result.job?.job.id;
5099
+ if (nextJobId) {
5100
+ enqueueWikiIngestJob(nextJobId);
5101
+ }
5102
+ reply.code(201);
5103
+ return result;
5104
+ }
5105
+ catch (error) {
5106
+ if (error instanceof Error &&
5107
+ error.message.includes("can only be rerun")) {
5108
+ reply.code(409);
5109
+ return { error: error.message };
5110
+ }
5111
+ throw error;
5112
+ }
5113
+ });
5114
+ app.post("/api/v1/wiki/ingest-jobs/:id/resume", async (request, reply) => {
5115
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/resume" });
5116
+ const { id } = request.params;
5117
+ const job = getWikiIngestJob(id);
5118
+ if (!job) {
5119
+ reply.code(404);
5120
+ return { error: "Wiki ingest job not found" };
5121
+ }
5122
+ const hasRecoverableOpenAiResponse = job.logs.some((entry) => typeof entry.metadata.responseId === "string") ||
5123
+ job.assets.some((asset) => typeof asset.metadata.openAiResponseId === "string" ||
5124
+ asset.status === "processing");
5125
+ const canResume = ["queued", "processing"].includes(job.job.status) ||
5126
+ (job.job.status === "failed" && hasRecoverableOpenAiResponse);
5127
+ if (!canResume) {
5128
+ reply.code(409);
5129
+ return {
5130
+ error: "Only active wiki ingest jobs, or failed jobs with a recoverable OpenAI background response, can be resumed.",
5131
+ job,
5132
+ resumed: false
5133
+ };
5134
+ }
5135
+ const alreadyActive = managers.backgroundJobs.has(id);
5136
+ if (!alreadyActive) {
5137
+ enqueueWikiIngestJob(id);
5138
+ }
5139
+ return {
5140
+ job: getWikiIngestJob(id),
5141
+ resumed: !alreadyActive
5142
+ };
5143
+ });
5144
+ app.delete("/api/v1/wiki/ingest-jobs/:id", async (request, reply) => {
5145
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id" });
5146
+ const { id } = request.params;
5147
+ try {
5148
+ const deleted = deleteWikiIngestJob(id);
5149
+ if (!deleted) {
5150
+ reply.code(404);
5151
+ return { error: "Wiki ingest job not found" };
5152
+ }
5153
+ return { deleted };
5154
+ }
5155
+ catch (error) {
5156
+ if (error instanceof Error &&
5157
+ error.message.includes("can only be deleted")) {
5158
+ reply.code(409);
5159
+ return { error: error.message };
5160
+ }
5161
+ throw error;
5162
+ }
5163
+ });
4873
5164
  app.post("/api/v1/wiki/ingest-jobs/:id/review", async (request, reply) => {
4874
5165
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/review" });
4875
5166
  const { id } = request.params;
4876
5167
  const reviewed = await reviewWikiIngestJob(id, reviewWikiIngestJobSchema.parse(request.body ?? {}), {
4877
5168
  createNote: (note) => createNote(note, toActivityContext(auth)),
4878
5169
  updateNote: (noteId, patch) => updateNote(noteId, patch, toActivityContext(auth)),
4879
- publishEntity: (proposal) => publishIngestProposalEntity(proposal, auth)
5170
+ publishEntity: (proposal) => publishIngestProposalEntity(proposal, auth),
5171
+ resolveMappedEntity: (entityType, entityId) => resolveMappedIngestEntity(entityType, entityId)
4880
5172
  });
4881
5173
  if (!reviewed) {
4882
5174
  reply.code(404);
@@ -5504,6 +5796,23 @@ export async function buildServer(options = {}) {
5504
5796
  reply.code(201);
5505
5797
  return event;
5506
5798
  });
5799
+ app.post("/api/v1/diagnostics/logs", async (request, reply) => {
5800
+ const payload = createDiagnosticLogSchema.parse(request.body ?? {});
5801
+ const entry = recordDiagnosticLog({
5802
+ ...payload,
5803
+ source: payload.source ??
5804
+ normalizeDiagnosticSource(request.headers["x-forge-source"])
5805
+ });
5806
+ reply.code(201);
5807
+ return { log: entry };
5808
+ });
5809
+ app.get("/api/v1/diagnostics/logs", async (request) => {
5810
+ requireOperatorSession(request.headers, {
5811
+ route: "/api/v1/diagnostics/logs"
5812
+ });
5813
+ const query = diagnosticLogListQuerySchema.parse(request.query ?? {});
5814
+ return listDiagnosticLogs(query);
5815
+ });
5507
5816
  app.get("/api/v1/events", async (request) => {
5508
5817
  const query = eventsListQuerySchema.parse(request.query ?? {});
5509
5818
  return { events: listEventLog(query) };
@@ -69,6 +69,10 @@ export const createCompanionPairingSessionSchema = z.object({
69
69
  "watch-ready"
70
70
  ])
71
71
  });
72
+ export const revokeAllCompanionPairingSessionsSchema = z.object({
73
+ userIds: z.array(z.string().trim().min(1)).default([]),
74
+ includeRevoked: z.boolean().default(false)
75
+ });
72
76
  export const mobileHealthSyncSchema = z.object({
73
77
  sessionId: z.string().trim().min(1),
74
78
  pairingToken: z.string().trim().min(1),
@@ -445,6 +449,35 @@ function listPairingRows(userIds) {
445
449
  ORDER BY updated_at DESC, created_at DESC`)
446
450
  .all(...params);
447
451
  }
452
+ function revokePairingRows(rows, activity) {
453
+ if (rows.length === 0) {
454
+ return [];
455
+ }
456
+ const now = nowIso();
457
+ const reason = activity?.reason ?? "Revoked by operator";
458
+ const revokeStatement = getDatabase().prepare(`UPDATE companion_pairing_sessions
459
+ SET status = 'revoked', last_sync_error = ?, updated_at = ?
460
+ WHERE id = ?`);
461
+ const refetchStatement = getDatabase().prepare(`SELECT * FROM companion_pairing_sessions WHERE id = ?`);
462
+ for (const row of rows) {
463
+ revokeStatement.run(reason, now, row.id);
464
+ recordActivityEvent({
465
+ entityType: "system",
466
+ entityId: row.id,
467
+ eventType: "companion_pairing_revoked",
468
+ title: "Companion pairing revoked",
469
+ description: "An operator revoked a Forge Companion pairing session and blocked further syncs for that device.",
470
+ actor: activity?.actor ?? null,
471
+ source: activity?.source ?? "ui",
472
+ metadata: {
473
+ label: row.label,
474
+ deviceName: row.device_name,
475
+ platform: row.platform
476
+ }
477
+ });
478
+ }
479
+ return rows.map((row) => mapPairingSession(refetchStatement.get(row.id)));
480
+ }
448
481
  function listHealthImportRunRows(userIds, limit = 12) {
449
482
  const params = [];
450
483
  const where = userIds && userIds.length > 0
@@ -472,43 +505,53 @@ export function revokeCompanionPairingSession(pairingSessionId, activity) {
472
505
  if (!current) {
473
506
  return undefined;
474
507
  }
475
- const now = nowIso();
476
- getDatabase()
477
- .prepare(`UPDATE companion_pairing_sessions
478
- SET status = 'revoked', last_sync_error = ?, updated_at = ?
479
- WHERE id = ?`)
480
- .run("Revoked by operator", now, pairingSessionId);
481
- recordActivityEvent({
482
- entityType: "system",
483
- entityId: pairingSessionId,
484
- eventType: "companion_pairing_revoked",
485
- title: "Companion pairing revoked",
486
- description: "An operator revoked a Forge Companion pairing session and blocked further syncs for that device.",
508
+ return revokePairingRows([current], activity)[0];
509
+ }
510
+ export function revokeAllCompanionPairingSessions(input, activity) {
511
+ const parsed = revokeAllCompanionPairingSessionsSchema.parse(input ?? {});
512
+ const rows = listPairingRows(parsed.userIds.length > 0 ? parsed.userIds : undefined)
513
+ .filter((row) => parsed.includeRevoked || row.status !== "revoked");
514
+ const sessions = revokePairingRows(rows, {
487
515
  actor: activity?.actor ?? null,
488
516
  source: activity?.source ?? "ui",
489
- metadata: {
490
- label: current.label,
491
- deviceName: current.device_name,
492
- platform: current.platform
493
- }
517
+ reason: "Revoked by operator (bulk)"
494
518
  });
495
- return mapPairingSession(getDatabase()
496
- .prepare(`SELECT * FROM companion_pairing_sessions WHERE id = ?`)
497
- .get(pairingSessionId));
519
+ return {
520
+ revokedCount: sessions.length,
521
+ sessions
522
+ };
498
523
  }
499
524
  export function createCompanionPairingSession(baseApiUrl, input) {
500
525
  const parsed = createCompanionPairingSessionSchema.parse(input);
501
526
  const now = new Date();
527
+ const userId = parsed.userId ?? "user_operator";
528
+ const serializedCapabilities = JSON.stringify(parsed.capabilities);
502
529
  const expiresAt = new Date(now.getTime() + parsed.expiresInMinutes * 60_000).toISOString();
503
530
  const id = `pair_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
504
531
  const pairingToken = randomUUID().replaceAll("-", "");
532
+ const stalePendingRows = getDatabase()
533
+ .prepare(`SELECT *
534
+ FROM companion_pairing_sessions
535
+ WHERE user_id = ?
536
+ AND label = ?
537
+ AND api_base_url = ?
538
+ AND capability_flags_json = ?
539
+ AND status = 'pending'`)
540
+ .all(userId, parsed.label, baseApiUrl, serializedCapabilities);
541
+ if (stalePendingRows.length > 0) {
542
+ revokePairingRows(stalePendingRows, {
543
+ actor: null,
544
+ source: "system",
545
+ reason: "Superseded by a newer pairing QR"
546
+ });
547
+ }
505
548
  getDatabase()
506
549
  .prepare(`INSERT INTO companion_pairing_sessions (
507
550
  id, user_id, label, pairing_token, status, capability_flags_json, api_base_url,
508
551
  expires_at, created_at, updated_at
509
552
  )
510
553
  VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)`)
511
- .run(id, parsed.userId ?? "user_operator", parsed.label, pairingToken, JSON.stringify(parsed.capabilities), baseApiUrl, expiresAt, now.toISOString(), now.toISOString());
554
+ .run(id, userId, parsed.label, pairingToken, serializedCapabilities, baseApiUrl, expiresAt, now.toISOString(), now.toISOString());
512
555
  const qrPayload = {
513
556
  kind: "forge-companion-pairing",
514
557
  apiBaseUrl: baseApiUrl,
@@ -539,6 +582,24 @@ export function verifyCompanionPairing(payload) {
539
582
  last_seen_at = ?, paired_at = COALESCE(paired_at, ?), updated_at = ?
540
583
  WHERE id = ?`)
541
584
  .run(nextStatus, parsed.device.name, parsed.device.platform, parsed.device.appVersion, now, now, now, pairing.id);
585
+ if (parsed.device.name.trim().length > 0) {
586
+ const duplicateRows = getDatabase()
587
+ .prepare(`SELECT *
588
+ FROM companion_pairing_sessions
589
+ WHERE user_id = ?
590
+ AND id != ?
591
+ AND status != 'revoked'
592
+ AND COALESCE(device_name, '') = ?
593
+ AND COALESCE(platform, '') = ?`)
594
+ .all(pairing.user_id, pairing.id, parsed.device.name, parsed.device.platform);
595
+ if (duplicateRows.length > 0) {
596
+ revokePairingRows(duplicateRows, {
597
+ actor: null,
598
+ source: "system",
599
+ reason: "Superseded by a newer verified device pairing"
600
+ });
601
+ }
602
+ }
542
603
  return {
543
604
  pairingSession: mapPairingSession(getDatabase()
544
605
  .prepare(`SELECT * FROM companion_pairing_sessions WHERE id = ?`)