forge-openclaw-plugin 0.2.20 → 0.2.21

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-B4A6TooJ.js +63 -0
  4. package/dist/assets/index-B4A6TooJ.js.map +1 -0
  5. package/dist/assets/index-D6Xs_2mo.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 +301 -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 +595 -62
  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-B4A6TooJ.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-D6Xs_2mo.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);
@@ -4553,6 +4714,28 @@ export async function buildServer(options = {}) {
4553
4714
  const readStringArrayField = (record, key) => Array.isArray(record[key])
4554
4715
  ? record[key].filter((entry) => typeof entry === "string" && entry.trim().length > 0)
4555
4716
  : [];
4717
+ const resolveMappedIngestEntity = (entityType, entityId) => {
4718
+ const result = searchEntities({
4719
+ searches: [
4720
+ {
4721
+ entityTypes: [entityType],
4722
+ ids: [entityId],
4723
+ includeDeleted: false,
4724
+ limit: 1
4725
+ }
4726
+ ]
4727
+ }).results[0];
4728
+ if (!result?.ok) {
4729
+ return null;
4730
+ }
4731
+ const match = result.matches?.find((entry) => entry.entityType === entityType && entry.id === entityId);
4732
+ return match
4733
+ ? {
4734
+ entityType,
4735
+ entityId
4736
+ }
4737
+ : null;
4738
+ };
4556
4739
  const publishIngestProposalEntity = (proposal, auth) => {
4557
4740
  const suggestedFields = proposal.suggestedFields &&
4558
4741
  typeof proposal.suggestedFields === "object" &&
@@ -4696,6 +4879,22 @@ export async function buildServer(options = {}) {
4696
4879
  }, toActivityContext(auth));
4697
4880
  return { entityType: "habit", entityId: habit.id };
4698
4881
  }
4882
+ case "psyche_value": {
4883
+ const value = createPsycheValue({
4884
+ title,
4885
+ description: summary,
4886
+ valuedDirection: readStringField(suggestedFields, "valuedDirection"),
4887
+ whyItMatters: readStringField(suggestedFields, "whyItMatters"),
4888
+ linkedGoalIds: readStringArrayField(suggestedFields, "linkedGoalIds"),
4889
+ linkedProjectIds: readStringArrayField(suggestedFields, "linkedProjectIds"),
4890
+ linkedTaskIds: readStringArrayField(suggestedFields, "linkedTaskIds"),
4891
+ committedActions: readStringArrayField(suggestedFields, "committedActions"),
4892
+ userId: typeof suggestedFields.userId === "string"
4893
+ ? suggestedFields.userId
4894
+ : null
4895
+ }, toActivityContext(auth));
4896
+ return { entityType: "psyche_value", entityId: value.id };
4897
+ }
4699
4898
  case "strategy": {
4700
4899
  const targetProjectIds = readStringArrayField(suggestedFields, "targetProjectIds");
4701
4900
  const linkedEntities = Array.isArray(suggestedFields.linkedEntities) &&
@@ -4826,13 +5025,7 @@ export async function buildServer(options = {}) {
4826
5025
  });
4827
5026
  const jobId = result.job?.job.id;
4828
5027
  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
- });
5028
+ enqueueWikiIngestJob(jobId);
4836
5029
  }
4837
5030
  reply.code(201);
4838
5031
  return result;
@@ -4849,13 +5042,7 @@ export async function buildServer(options = {}) {
4849
5042
  });
4850
5043
  const jobId = result.job?.job.id;
4851
5044
  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
- });
5045
+ enqueueWikiIngestJob(jobId);
4859
5046
  }
4860
5047
  reply.code(201);
4861
5048
  return result;
@@ -4870,13 +5057,91 @@ export async function buildServer(options = {}) {
4870
5057
  }
4871
5058
  return job;
4872
5059
  });
5060
+ app.post("/api/v1/wiki/ingest-jobs/:id/rerun", async (request, reply) => {
5061
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/rerun" });
5062
+ const { id } = request.params;
5063
+ try {
5064
+ const result = await rerunWikiIngestJob(id, {
5065
+ actor: requireAuthenticatedActor(request.headers, { route: "/api/v1/wiki/ingest-jobs/:id/rerun" }).actor ?? null
5066
+ });
5067
+ if (!result) {
5068
+ reply.code(404);
5069
+ return { error: "Wiki ingest job not found" };
5070
+ }
5071
+ const nextJobId = result.job?.job.id;
5072
+ if (nextJobId) {
5073
+ enqueueWikiIngestJob(nextJobId);
5074
+ }
5075
+ reply.code(201);
5076
+ return result;
5077
+ }
5078
+ catch (error) {
5079
+ if (error instanceof Error &&
5080
+ error.message.includes("can only be rerun")) {
5081
+ reply.code(409);
5082
+ return { error: error.message };
5083
+ }
5084
+ throw error;
5085
+ }
5086
+ });
5087
+ app.post("/api/v1/wiki/ingest-jobs/:id/resume", async (request, reply) => {
5088
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/resume" });
5089
+ const { id } = request.params;
5090
+ const job = getWikiIngestJob(id);
5091
+ if (!job) {
5092
+ reply.code(404);
5093
+ return { error: "Wiki ingest job not found" };
5094
+ }
5095
+ const hasRecoverableOpenAiResponse = job.logs.some((entry) => typeof entry.metadata.responseId === "string") ||
5096
+ job.assets.some((asset) => typeof asset.metadata.openAiResponseId === "string" ||
5097
+ asset.status === "processing");
5098
+ const canResume = ["queued", "processing"].includes(job.job.status) ||
5099
+ (job.job.status === "failed" && hasRecoverableOpenAiResponse);
5100
+ if (!canResume) {
5101
+ reply.code(409);
5102
+ return {
5103
+ error: "Only active wiki ingest jobs, or failed jobs with a recoverable OpenAI background response, can be resumed.",
5104
+ job,
5105
+ resumed: false
5106
+ };
5107
+ }
5108
+ const alreadyActive = managers.backgroundJobs.has(id);
5109
+ if (!alreadyActive) {
5110
+ enqueueWikiIngestJob(id);
5111
+ }
5112
+ return {
5113
+ job: getWikiIngestJob(id),
5114
+ resumed: !alreadyActive
5115
+ };
5116
+ });
5117
+ app.delete("/api/v1/wiki/ingest-jobs/:id", async (request, reply) => {
5118
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id" });
5119
+ const { id } = request.params;
5120
+ try {
5121
+ const deleted = deleteWikiIngestJob(id);
5122
+ if (!deleted) {
5123
+ reply.code(404);
5124
+ return { error: "Wiki ingest job not found" };
5125
+ }
5126
+ return { deleted };
5127
+ }
5128
+ catch (error) {
5129
+ if (error instanceof Error &&
5130
+ error.message.includes("can only be deleted")) {
5131
+ reply.code(409);
5132
+ return { error: error.message };
5133
+ }
5134
+ throw error;
5135
+ }
5136
+ });
4873
5137
  app.post("/api/v1/wiki/ingest-jobs/:id/review", async (request, reply) => {
4874
5138
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/review" });
4875
5139
  const { id } = request.params;
4876
5140
  const reviewed = await reviewWikiIngestJob(id, reviewWikiIngestJobSchema.parse(request.body ?? {}), {
4877
5141
  createNote: (note) => createNote(note, toActivityContext(auth)),
4878
5142
  updateNote: (noteId, patch) => updateNote(noteId, patch, toActivityContext(auth)),
4879
- publishEntity: (proposal) => publishIngestProposalEntity(proposal, auth)
5143
+ publishEntity: (proposal) => publishIngestProposalEntity(proposal, auth),
5144
+ resolveMappedEntity: (entityType, entityId) => resolveMappedIngestEntity(entityType, entityId)
4880
5145
  });
4881
5146
  if (!reviewed) {
4882
5147
  reply.code(404);
@@ -5504,6 +5769,23 @@ export async function buildServer(options = {}) {
5504
5769
  reply.code(201);
5505
5770
  return event;
5506
5771
  });
5772
+ app.post("/api/v1/diagnostics/logs", async (request, reply) => {
5773
+ const payload = createDiagnosticLogSchema.parse(request.body ?? {});
5774
+ const entry = recordDiagnosticLog({
5775
+ ...payload,
5776
+ source: payload.source ??
5777
+ normalizeDiagnosticSource(request.headers["x-forge-source"])
5778
+ });
5779
+ reply.code(201);
5780
+ return { log: entry };
5781
+ });
5782
+ app.get("/api/v1/diagnostics/logs", async (request) => {
5783
+ requireOperatorSession(request.headers, {
5784
+ route: "/api/v1/diagnostics/logs"
5785
+ });
5786
+ const query = diagnosticLogListQuerySchema.parse(request.query ?? {});
5787
+ return listDiagnosticLogs(query);
5788
+ });
5507
5789
  app.get("/api/v1/events", async (request) => {
5508
5790
  const query = eventsListQuerySchema.parse(request.query ?? {});
5509
5791
  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 = ?`)