forge-openclaw-plugin 0.2.48 → 0.2.50
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/README.md +3 -3
- package/dist/assets/index-2_tuemtU.css +1 -0
- package/dist/assets/index-C9_gJvi6.js +91 -0
- package/dist/assets/index-C9_gJvi6.js.map +1 -0
- package/dist/index.html +2 -2
- package/dist/openclaw/parity.js +14 -0
- package/dist/openclaw/routes.js +42 -0
- package/dist/openclaw/session-registry.js +17 -0
- package/dist/openclaw/tools.js +3 -3
- package/dist/server/server/migrations/019_wiki_memory.sql +1 -1
- package/dist/server/server/migrations/052_agent_identity_tightening.sql +307 -0
- package/dist/server/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
- package/dist/server/server/migrations/054_sqlite_backed_wiki_memory.sql +8 -0
- package/dist/server/server/src/app.js +46 -14
- package/dist/server/server/src/db.js +0 -2
- package/dist/server/server/src/openapi.js +58 -3
- package/dist/server/server/src/repositories/agent-runtime-sessions.js +122 -16
- package/dist/server/server/src/repositories/model-settings.js +5 -0
- package/dist/server/server/src/repositories/notes.js +5 -2
- package/dist/server/server/src/repositories/settings.js +101 -13
- package/dist/server/server/src/repositories/users.js +23 -0
- package/dist/server/server/src/repositories/wiki-memory.js +16 -190
- package/dist/server/server/src/services/data-management.js +2 -9
- package/dist/server/server/src/types.js +13 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -2
- package/server/migrations/019_wiki_memory.sql +1 -1
- package/server/migrations/052_agent_identity_tightening.sql +307 -0
- package/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
- package/server/migrations/054_sqlite_backed_wiki_memory.sql +8 -0
- package/skills/forge-openclaw/SKILL.md +6 -6
- package/skills/forge-openclaw/entity_conversation_playbooks.md +49 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +32 -0
- package/dist/assets/index-Bv9FWWsZ.js +0 -91
- package/dist/assets/index-Bv9FWWsZ.js.map +0 -1
- package/dist/assets/index-DtEvFzXp.css +0 -1
|
@@ -2643,7 +2643,7 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
2643
2643
|
}),
|
|
2644
2644
|
enrichOnboardingEntityGuide({
|
|
2645
2645
|
entityType: "wiki_page",
|
|
2646
|
-
purpose: "A
|
|
2646
|
+
purpose: "A SQLite-backed Forge wiki page or evidence page.",
|
|
2647
2647
|
minimumCreateFields: ["title", "contentMarkdown"],
|
|
2648
2648
|
relationshipRules: [
|
|
2649
2649
|
"Wiki pages live on the wiki surface and use specialized page upsert routes rather than batch CRUD.",
|
|
@@ -2772,6 +2772,12 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
|
|
|
2772
2772
|
"When useful, help the user name, define, and connect the record in that order: offer a working label, clarify what belongs inside it, then ask about links only after the record itself feels steady.",
|
|
2773
2773
|
"When the meaning is clearer than the wording, offer a tentative title or formulation yourself and invite correction instead of forcing the user to wordsmith alone.",
|
|
2774
2774
|
"For direct update or review requests, the next question should usually narrow the saved object, timeframe, or route family instead of reopening the whole meaning-making arc.",
|
|
2775
|
+
"For updates, start with the smallest thing that now feels wrong, newly true, or newly visible rather than restarting the whole story.",
|
|
2776
|
+
"For review requests, ask what practical question the user wants the read to answer before you ask for more scope.",
|
|
2777
|
+
"The opening question should help the user understand what they are actually trying to save, decide, review, or change, not make them perform the schema out loud.",
|
|
2778
|
+
"If the user already named the exact correction in usable language, confirm only the missing scope, timing, or route-selecting detail that still matters, then act.",
|
|
2779
|
+
"Once a specialized-surface lane is clear, speak in route-relevant nouns such as timeline, overlay, weekday template, published output, run detail, or node result instead of generic record language.",
|
|
2780
|
+
"If the next answer would not change the route, wording, timing, or write payload in a meaningful way, stop asking and act.",
|
|
2775
2781
|
"Before saving, briefly summarize the working formulation in the user's own language when that would reduce ambiguity.",
|
|
2776
2782
|
"Once the record is clear enough to name, stop exploring broadly and ask only for the last structural detail that still matters.",
|
|
2777
2783
|
"If the record is already clear enough to save, save it instead of performing a ceremonial extra question.",
|
|
@@ -3546,7 +3552,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
3546
3552
|
requiredFields: [],
|
|
3547
3553
|
notes: [
|
|
3548
3554
|
"Semantic search is optional and profile-driven.",
|
|
3549
|
-
"The wiki is
|
|
3555
|
+
"The wiki is SQLite-backed, so pages and evidence live in Forge's database."
|
|
3550
3556
|
],
|
|
3551
3557
|
example: "{}"
|
|
3552
3558
|
},
|
|
@@ -3588,13 +3594,13 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
3588
3594
|
},
|
|
3589
3595
|
{
|
|
3590
3596
|
toolName: "forge_upsert_wiki_page",
|
|
3591
|
-
summary: "Create a new wiki page or update an existing one through the
|
|
3597
|
+
summary: "Create a new wiki page or update an existing one through the SQLite-backed wiki surface.",
|
|
3592
3598
|
whenToUse: "Use when the user explicitly wants wiki memory persisted or reorganized.",
|
|
3593
3599
|
inputShape: '{ pageId?: string, kind?: "wiki"|"evidence", title: string, slug?: string, summary?: string, aliases?: string[], contentMarkdown: string, author?: string|null, tags?: string[], spaceId?: string, frontmatter?: object, links?: Array<{ entityType, entityId, anchorKey? }> }',
|
|
3594
3600
|
requiredFields: ["title", "contentMarkdown"],
|
|
3595
3601
|
notes: [
|
|
3596
3602
|
"When pageId is omitted, Forge creates a new page.",
|
|
3597
|
-
"When pageId is present, Forge patches the existing
|
|
3603
|
+
"When pageId is present, Forge patches the existing SQLite note record."
|
|
3598
3604
|
],
|
|
3599
3605
|
example: '{"title":"Taste map","contentMarkdown":"# Taste map\\n\\n[[forge:goal:goal_123|Core goal]] influences this page.","spaceId":"wiki_space_shared"}'
|
|
3600
3606
|
},
|
|
@@ -3605,19 +3611,19 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
3605
3611
|
inputShape: "{ spaceId?: string }",
|
|
3606
3612
|
requiredFields: [],
|
|
3607
3613
|
notes: [
|
|
3608
|
-
"This is the explicit health surface for the
|
|
3614
|
+
"This is the explicit health surface for the SQLite-backed wiki memory layer.",
|
|
3609
3615
|
"Use it before proposing cleanup work or auto-maintenance."
|
|
3610
3616
|
],
|
|
3611
3617
|
example: '{"spaceId":"wiki_space_shared"}'
|
|
3612
3618
|
},
|
|
3613
3619
|
{
|
|
3614
3620
|
toolName: "forge_sync_wiki_vault",
|
|
3615
|
-
summary: "
|
|
3616
|
-
whenToUse: "Use after
|
|
3621
|
+
summary: "Rebuild SQLite wiki search, link, and metadata indexes.",
|
|
3622
|
+
whenToUse: "Use after large SQLite wiki changes or maintenance work that should refresh derived metadata.",
|
|
3617
3623
|
inputShape: "{ spaceId?: string }",
|
|
3618
3624
|
requiredFields: [],
|
|
3619
3625
|
notes: [
|
|
3620
|
-
"Forge treats
|
|
3626
|
+
"Forge treats SQLite as the canonical wiki store; this route refreshes derived indexes."
|
|
3621
3627
|
],
|
|
3622
3628
|
example: '{"spaceId":"wiki_space_shared"}'
|
|
3623
3629
|
},
|
|
@@ -3636,11 +3642,11 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
3636
3642
|
{
|
|
3637
3643
|
toolName: "forge_ingest_wiki_source",
|
|
3638
3644
|
summary: "Ingest raw text, local files, or URLs into the wiki, preserving a raw source artifact and returning page plus proposal outputs.",
|
|
3639
|
-
whenToUse: "Use when the operator wants source material compiled into
|
|
3645
|
+
whenToUse: "Use when the operator wants source material compiled into SQLite-backed wiki memory and optional Forge-entity proposals.",
|
|
3640
3646
|
inputShape: '{ spaceId?: string, titleHint?: string, sourceKind: "raw_text"|"local_path"|"url", sourceText?: string, sourcePath?: string, sourceUrl?: string, mimeType?: string, llmProfileId?: string, parseStrategy?: "auto"|"text_only"|"multimodal", entityProposalMode?: "none"|"suggest", createAsKind?: "wiki"|"evidence", linkedEntityHints?: Array<{ entityType, entityId, anchorKey? }> }',
|
|
3641
3647
|
requiredFields: ["sourceKind", "sourceText/sourcePath/sourceUrl"],
|
|
3642
3648
|
notes: [
|
|
3643
|
-
"Forge preserves a raw artifact
|
|
3649
|
+
"Forge preserves a raw ingest artifact separately from SQLite page content.",
|
|
3644
3650
|
"Entity proposals are suggestions only; they are not auto-applied."
|
|
3645
3651
|
],
|
|
3646
3652
|
example: '{"sourceKind":"url","sourceUrl":"https://example.com/article","titleHint":"Research import","parseStrategy":"auto","entityProposalMode":"suggest"}'
|
|
@@ -4013,7 +4019,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4013
4019
|
task: "A concrete actionable work item. Task status is board state, not proof of live work.",
|
|
4014
4020
|
taskRun: "A live work session attached to a task. Start, heartbeat, focus, complete, and release runs instead of faking work with status alone.",
|
|
4015
4021
|
note: "A Markdown work note that can link to one or many entities. Use notes for progress evidence, context, and close-out summaries.",
|
|
4016
|
-
wiki: "Forge Wiki is the
|
|
4022
|
+
wiki: "Forge Wiki is the SQLite-backed memory layer: Markdown content in notes rows plus media, backlinks, optional embeddings, explicit spaces, and structured links back to Forge entities.",
|
|
4017
4023
|
sleepSession: "A sleep session is a first-class health record with timing, sleep and bed duration, stage breakdown, recovery metrics, annotations, and Forge links back to planning or Psyche context.",
|
|
4018
4024
|
workoutSession: "A workout session is a first-class sports record imported from HealthKit or generated from a habit. It holds workout type, timing, energy or distance when available, subjective effort, narrative context, and Forge links.",
|
|
4019
4025
|
preferences: "Forge Preferences is the explicit taste-modeling domain. It has workspaces, contexts, concept libraries, direct items, pairwise judgments, direct signals, and inferred scores.",
|
|
@@ -4157,6 +4163,11 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4157
4163
|
specializedDomainSurfaces: {
|
|
4158
4164
|
movement: {
|
|
4159
4165
|
summary: "Dedicated movement workspace API. Use these routes for stays, trips, time-in-place questions, visited places, trip detail, selection aggregates, user-defined overlays, and repair actions on already-recorded movement data.",
|
|
4166
|
+
routeSelectionQuestions: [
|
|
4167
|
+
"Is the user asking for a day, month, all-time, timeline, place, trip detail, or selected-span answer?",
|
|
4168
|
+
"Is this a missing-gap overlay, a saved-overlay repair, or an edit to one already-recorded stay, trip, or trip point?",
|
|
4169
|
+
"If the target is already known, what one time, place, or saved-object detail is still missing before acting?"
|
|
4170
|
+
],
|
|
4160
4171
|
readRoutes: {
|
|
4161
4172
|
day: "/api/v1/movement/day",
|
|
4162
4173
|
month: "/api/v1/movement/month",
|
|
@@ -4194,6 +4205,11 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4194
4205
|
},
|
|
4195
4206
|
lifeForce: {
|
|
4196
4207
|
summary: "Dedicated life-force API. Use it to read the current energy budget, drains, recommendations, and warnings, then patch only the parts that are meant to be user-controlled.",
|
|
4208
|
+
routeSelectionQuestions: [
|
|
4209
|
+
"Is the user trying to understand the overview, change durable profile assumptions, change a weekday curve, or log a right-now fatigue signal?",
|
|
4210
|
+
"Are they describing a repeatable weekly shape or a one-off current state?",
|
|
4211
|
+
"If the lane is already clear, what one weekday, profile field, or signal detail is still missing?"
|
|
4212
|
+
],
|
|
4197
4213
|
readRoutes: {
|
|
4198
4214
|
overview: "/api/v1/life-force"
|
|
4199
4215
|
},
|
|
@@ -4206,12 +4222,18 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4206
4222
|
"Life Force is a focused domain surface, not a batch CRUD entity type.",
|
|
4207
4223
|
"Use GET /api/v1/life-force for the current overview payload with stats, drains, recommendations, and current-curve state.",
|
|
4208
4224
|
"Patch the profile only for durable personal settings, update weekday templates only for the curve itself, and post fatigue signals for real-time tired or recovered observations.",
|
|
4225
|
+
"If the user says something like 'I always dip on Tuesdays after lunch', treat that as a weekday-template change rather than a one-off fatigue signal.",
|
|
4209
4226
|
"If the user is asking what changed after a profile, template, or fatigue write, read the overview back so the effect stays visible.",
|
|
4210
4227
|
"If the user already knows they want a profile change, weekday-template edit, or right-now fatigue signal, skip the broad lane question and ask only for the missing weekday, profile field, or signal detail."
|
|
4211
4228
|
]
|
|
4212
4229
|
},
|
|
4213
4230
|
workbench: {
|
|
4214
4231
|
summary: "Dedicated graph-flow API. Use it for flow catalog reads, flow CRUD, execution, run history, published outputs, node results, and latest successful node outputs.",
|
|
4232
|
+
routeSelectionQuestions: [
|
|
4233
|
+
"Is the job flow discovery, flow editing, execution, published output, run detail, node result, latest node output, or flow chat follow-up?",
|
|
4234
|
+
"Does the user need a stable public contract or one execution artifact?",
|
|
4235
|
+
"If the flow is already known, what one run, node, or output scope detail is still missing before acting?"
|
|
4236
|
+
],
|
|
4215
4237
|
readRoutes: {
|
|
4216
4238
|
listFlows: "/api/v1/workbench/flows",
|
|
4217
4239
|
flowById: "/api/v1/workbench/flows/:id",
|
|
@@ -4237,6 +4259,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4237
4259
|
"Use the flow routes when the agent needs stable public input contracts, published outputs, node-level results, or reusable execution history.",
|
|
4238
4260
|
"If the user is still figuring out inputs or editable structure, read flow detail or box catalog before asking them to author a payload from memory.",
|
|
4239
4261
|
"Prefer the dedicated output and node-result routes over reverse-engineering raw traces.",
|
|
4262
|
+
"If the user only wants a published output, latest node output, or run detail, do not reopen a flow-edit intake before reading that artifact.",
|
|
4240
4263
|
"If the user already named the flow and wants one output or one run, skip the broad lane question and ask only for the missing run, node, or output scope."
|
|
4241
4264
|
]
|
|
4242
4265
|
}
|
|
@@ -4358,17 +4381,26 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4358
4381
|
movementTimeline: "/api/v1/movement/timeline",
|
|
4359
4382
|
movementAllTime: "/api/v1/movement/all-time",
|
|
4360
4383
|
movementPlaces: "/api/v1/movement/places",
|
|
4384
|
+
movementBoxDetail: "/api/v1/movement/boxes/:id",
|
|
4385
|
+
movementSettings: "/api/v1/movement/settings",
|
|
4386
|
+
movementSettingsUpdate: "/api/v1/movement/settings",
|
|
4361
4387
|
movementTripDetail: "/api/v1/movement/trips/:id",
|
|
4362
4388
|
movementSelection: "/api/v1/movement/selection",
|
|
4363
4389
|
movementUserBoxPreflight: "/api/v1/movement/user-boxes/preflight",
|
|
4364
4390
|
movementUserBoxUpdate: "/api/v1/movement/user-boxes/:id",
|
|
4391
|
+
movementUserBoxDelete: "/api/v1/movement/user-boxes/:id",
|
|
4365
4392
|
movementAutomaticBoxInvalidate: "/api/v1/movement/automatic-boxes/:id/invalidate",
|
|
4366
4393
|
movementStayUpdate: "/api/v1/movement/stays/:id",
|
|
4394
|
+
movementStayDelete: "/api/v1/movement/stays/:id",
|
|
4367
4395
|
movementTripUpdate: "/api/v1/movement/trips/:id",
|
|
4396
|
+
movementTripDelete: "/api/v1/movement/trips/:id",
|
|
4368
4397
|
movementTripPointUpdate: "/api/v1/movement/trips/:id/points/:pointId",
|
|
4398
|
+
movementTripPointDelete: "/api/v1/movement/trips/:id/points/:pointId",
|
|
4399
|
+
workbenchBoxCatalog: "/api/v1/workbench/catalog/boxes",
|
|
4369
4400
|
workbenchFlows: "/api/v1/workbench/flows",
|
|
4370
4401
|
workbenchFlowBySlug: "/api/v1/workbench/flows/by-slug/:slug",
|
|
4371
4402
|
workbenchPublishedOutput: "/api/v1/workbench/flows/:id/output",
|
|
4403
|
+
workbenchRuns: "/api/v1/workbench/flows/:id/runs",
|
|
4372
4404
|
workbenchRunDetail: "/api/v1/workbench/flows/:id/runs/:runId",
|
|
4373
4405
|
workbenchNodeResult: "/api/v1/workbench/flows/:id/runs/:runId/nodes/:nodeId",
|
|
4374
4406
|
workbenchLatestNodeOutput: "/api/v1/workbench/flows/:id/nodes/:nodeId/output",
|
|
@@ -4444,9 +4476,9 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4444
4476
|
saveSuggestionPlacement: "end_of_message",
|
|
4445
4477
|
saveSuggestionTone: "gentle_optional",
|
|
4446
4478
|
maxQuestionsPerTurn: 1,
|
|
4447
|
-
psycheExplorationRule: "When a Psyche entity needs understanding first, begin with one exploratory question before any working formulation, replacement belief, suggested title, or save pitch. Keep the opening reflection to one or two short sentences, stay in plain prose instead of bullets or numbered lists, keep that first reply short, do not mention Forge search or save structure yet, avoid colons or list-shaped phrasing, prefer what/when/how over why until the experience is grounded, wait for the user's answer before offering a fuller formulation, ask permission before moving from charged exploration into naming or challenge when needed, do not widen into adjacent entities until the current one has a working sentence the user recognizes, and once the lived experience is coherent stop deepening and help the user name it cleanly. When the user is updating a Psyche record because of one fresh episode, anchor in that episode before renaming the durable formulation. If the user accepts the wording, move toward the save instead of reopening deeper exploration.",
|
|
4448
|
-
specializedSurfaceRule: "For Movement, Life Force, and Workbench, clarify the lane first, then name the dedicated route family in plain language and do not guess at a generic CRUD path. If the truth of the current state is still uncertain, read the relevant specialized view before you mutate it. When the user already named a precise correction or review target, confirm only the route-selecting detail that is still missing. After a concrete specialized-surface correction, read the relevant specialized view back when the user is trying to understand the result rather than just store it. The canonical runtime routes stay under /api/v1/*, and the OpenClaw HTTP mirror exposes the same families under /forge/v1/movement, /forge/v1/life-force, and /forge/v1/workbench.",
|
|
4449
|
-
reviewShortcutRule: "When the user is reviewing or correcting an existing record, narrow the saved object, timeframe, or route family first. Do not reopen the whole intake unless the user is actually redefining the record.",
|
|
4479
|
+
psycheExplorationRule: "When a Psyche entity needs understanding first, begin with one exploratory question before any working formulation, replacement belief, suggested title, or save pitch. Keep the opening reflection to one or two short sentences, stay in plain prose instead of bullets or numbered lists, keep that first reply short, do not mention Forge search or save structure yet, avoid colons or list-shaped phrasing, prefer what/when/how over why until the experience is grounded, wait for the user's answer before offering a fuller formulation, ask permission before moving from charged exploration into naming or challenge when needed, make the next question help the user feel more able to name the experience rather than more examined, do not widen into adjacent entities until the current one has a working sentence the user recognizes, and once the lived experience is coherent stop deepening and help the user name it cleanly. When the user is updating a Psyche record because of one fresh episode, anchor in that episode before renaming the durable formulation, begin with the smallest part of the old wording that no longer fits, and do not reopen the full origin story unless the new understanding is truly structural. If the user accepts the wording, move toward the save instead of reopening deeper exploration.",
|
|
4480
|
+
specializedSurfaceRule: "For Movement, Life Force, and Workbench, clarify the lane first, then name the dedicated route family in plain language and do not guess at a generic CRUD path. Use specializedDomainSurfaces.routeSelectionQuestions when they are present so the next follow-up stays route-selective instead of generic. Once the lane is clear, talk in route-relevant nouns such as timeline, overlay, weekday template, published output, run detail, or node result rather than generic record language. If the truth of the current state is still uncertain, read the relevant specialized view before you mutate it. When the user already named a precise correction or review target, confirm only the route-selecting detail that is still missing. After a concrete specialized-surface correction, read the relevant specialized view back when the user is trying to understand the result rather than just store it. The canonical runtime routes stay under /api/v1/*, and the OpenClaw HTTP mirror exposes the same families under /forge/v1/movement, /forge/v1/life-force, and /forge/v1/workbench.",
|
|
4481
|
+
reviewShortcutRule: "When the user is reviewing or correcting an existing record, ask what practical question they want the read or correction to answer, then narrow the saved object, timeframe, or route family first. Do not reopen the whole intake unless the user is actually redefining the record.",
|
|
4450
4482
|
readModelWriteRule: "Self-observation is note-backed and should be written through observed notes with frontmatter.observedAt. Sleep and workout sessions stay on batch CRUD by default; use the reflective review helpers only when enriching one already-known record after review.",
|
|
4451
4483
|
psycheOpeningQuestionRule: "Prefer a concrete opening question tied to the entity: ask when the value mattered, what happened the last time the pattern appeared, what cue or body signal came first before the behavior, what the belief starts saying about self or outcome, what feels most at risk inside the mode, what the part is trying to get the user to do or stop doing, or where the shift began in the incident. Reflect briefly before the question, choose one follow-up lane at a time, say what is becoming clearer before the next deeper question, and if several Psyche entities are visible hold the adjacent ones lightly until the main container is clear.",
|
|
4452
4484
|
duplicateCheckRoute: "/api/v1/entities/search",
|
|
@@ -34,14 +34,12 @@ function resolveLegacyDatabasePath(root = dataRoot) {
|
|
|
34
34
|
function hasCanonicalRuntimeLayout(root = dataRoot) {
|
|
35
35
|
const canonicalRoot = resolveCanonicalDataDir(root);
|
|
36
36
|
return (existsSync(resolveCanonicalDatabasePath(root)) ||
|
|
37
|
-
existsSync(path.join(canonicalRoot, "wiki")) ||
|
|
38
37
|
existsSync(path.join(canonicalRoot, "wiki-ingest")) ||
|
|
39
38
|
existsSync(path.join(canonicalRoot, ".forge-secrets.key")));
|
|
40
39
|
}
|
|
41
40
|
function hasLegacyRuntimeLayout(root = dataRoot) {
|
|
42
41
|
const legacyRoot = resolveLegacyDataDir(root);
|
|
43
42
|
return (existsSync(resolveLegacyDatabasePath(root)) ||
|
|
44
|
-
existsSync(path.join(legacyRoot, "wiki")) ||
|
|
45
43
|
existsSync(path.join(legacyRoot, "wiki-ingest")) ||
|
|
46
44
|
existsSync(path.join(legacyRoot, ".forge-secrets.key")));
|
|
47
45
|
}
|
|
@@ -2137,6 +2137,11 @@ export function buildOpenApiDocument() {
|
|
|
2137
2137
|
"id",
|
|
2138
2138
|
"label",
|
|
2139
2139
|
"agentType",
|
|
2140
|
+
"identityKey",
|
|
2141
|
+
"provider",
|
|
2142
|
+
"machineKey",
|
|
2143
|
+
"personaKey",
|
|
2144
|
+
"linkedUsers",
|
|
2140
2145
|
"trustLevel",
|
|
2141
2146
|
"autonomyMode",
|
|
2142
2147
|
"approvalMode",
|
|
@@ -2150,6 +2155,20 @@ export function buildOpenApiDocument() {
|
|
|
2150
2155
|
id: { type: "string" },
|
|
2151
2156
|
label: { type: "string" },
|
|
2152
2157
|
agentType: { type: "string" },
|
|
2158
|
+
identityKey: nullable({ type: "string" }),
|
|
2159
|
+
provider: nullable({ type: "string", enum: ["openclaw", "hermes", "codex"] }),
|
|
2160
|
+
machineKey: nullable({ type: "string" }),
|
|
2161
|
+
personaKey: nullable({ type: "string" }),
|
|
2162
|
+
linkedUsers: arrayOf({
|
|
2163
|
+
type: "object",
|
|
2164
|
+
additionalProperties: false,
|
|
2165
|
+
required: ["userId", "role", "user"],
|
|
2166
|
+
properties: {
|
|
2167
|
+
userId: { type: "string" },
|
|
2168
|
+
role: { type: "string" },
|
|
2169
|
+
user: nullable({ $ref: "#/components/schemas/UserSummary" })
|
|
2170
|
+
}
|
|
2171
|
+
}),
|
|
2153
2172
|
trustLevel: {
|
|
2154
2173
|
type: "string",
|
|
2155
2174
|
enum: ["standard", "trusted", "autonomous"]
|
|
@@ -3234,17 +3253,26 @@ export function buildOpenApiDocument() {
|
|
|
3234
3253
|
"movementTimeline",
|
|
3235
3254
|
"movementAllTime",
|
|
3236
3255
|
"movementPlaces",
|
|
3256
|
+
"movementBoxDetail",
|
|
3257
|
+
"movementSettings",
|
|
3258
|
+
"movementSettingsUpdate",
|
|
3237
3259
|
"movementTripDetail",
|
|
3238
3260
|
"movementSelection",
|
|
3239
3261
|
"movementUserBoxPreflight",
|
|
3240
3262
|
"movementUserBoxUpdate",
|
|
3263
|
+
"movementUserBoxDelete",
|
|
3241
3264
|
"movementAutomaticBoxInvalidate",
|
|
3242
3265
|
"movementStayUpdate",
|
|
3266
|
+
"movementStayDelete",
|
|
3243
3267
|
"movementTripUpdate",
|
|
3268
|
+
"movementTripDelete",
|
|
3244
3269
|
"movementTripPointUpdate",
|
|
3270
|
+
"movementTripPointDelete",
|
|
3271
|
+
"workbenchBoxCatalog",
|
|
3245
3272
|
"workbenchFlows",
|
|
3246
3273
|
"workbenchFlowBySlug",
|
|
3247
3274
|
"workbenchPublishedOutput",
|
|
3275
|
+
"workbenchRuns",
|
|
3248
3276
|
"workbenchRunDetail",
|
|
3249
3277
|
"workbenchNodeResult",
|
|
3250
3278
|
"workbenchLatestNodeOutput",
|
|
@@ -3273,17 +3301,26 @@ export function buildOpenApiDocument() {
|
|
|
3273
3301
|
movementTimeline: { type: "string" },
|
|
3274
3302
|
movementAllTime: { type: "string" },
|
|
3275
3303
|
movementPlaces: { type: "string" },
|
|
3304
|
+
movementBoxDetail: { type: "string" },
|
|
3305
|
+
movementSettings: { type: "string" },
|
|
3306
|
+
movementSettingsUpdate: { type: "string" },
|
|
3276
3307
|
movementTripDetail: { type: "string" },
|
|
3277
3308
|
movementSelection: { type: "string" },
|
|
3278
3309
|
movementUserBoxPreflight: { type: "string" },
|
|
3279
3310
|
movementUserBoxUpdate: { type: "string" },
|
|
3311
|
+
movementUserBoxDelete: { type: "string" },
|
|
3280
3312
|
movementAutomaticBoxInvalidate: { type: "string" },
|
|
3281
3313
|
movementStayUpdate: { type: "string" },
|
|
3314
|
+
movementStayDelete: { type: "string" },
|
|
3282
3315
|
movementTripUpdate: { type: "string" },
|
|
3316
|
+
movementTripDelete: { type: "string" },
|
|
3283
3317
|
movementTripPointUpdate: { type: "string" },
|
|
3318
|
+
movementTripPointDelete: { type: "string" },
|
|
3319
|
+
workbenchBoxCatalog: { type: "string" },
|
|
3284
3320
|
workbenchFlows: { type: "string" },
|
|
3285
3321
|
workbenchFlowBySlug: { type: "string" },
|
|
3286
3322
|
workbenchPublishedOutput: { type: "string" },
|
|
3323
|
+
workbenchRuns: { type: "string" },
|
|
3287
3324
|
workbenchRunDetail: { type: "string" },
|
|
3288
3325
|
workbenchNodeResult: { type: "string" },
|
|
3289
3326
|
workbenchLatestNodeOutput: { type: "string" },
|
|
@@ -4811,6 +4848,24 @@ export function buildOpenApiDocument() {
|
|
|
4811
4848
|
}
|
|
4812
4849
|
}
|
|
4813
4850
|
},
|
|
4851
|
+
"/api/v1/movement/boxes/{id}": {
|
|
4852
|
+
get: {
|
|
4853
|
+
summary: "Read one movement box with projected detail, provenance, and raw linked evidence",
|
|
4854
|
+
responses: {
|
|
4855
|
+
"200": jsonResponse({
|
|
4856
|
+
type: "object",
|
|
4857
|
+
required: ["movement"],
|
|
4858
|
+
properties: {
|
|
4859
|
+
movement: {
|
|
4860
|
+
type: "object",
|
|
4861
|
+
additionalProperties: true
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
}, "Movement box detail"),
|
|
4865
|
+
"404": { $ref: "#/components/responses/Error" }
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
},
|
|
4814
4869
|
"/api/v1/movement/trips/{id}": {
|
|
4815
4870
|
get: {
|
|
4816
4871
|
summary: "Read one movement trip with its full detail",
|
|
@@ -5241,7 +5296,7 @@ export function buildOpenApiDocument() {
|
|
|
5241
5296
|
}
|
|
5242
5297
|
},
|
|
5243
5298
|
post: {
|
|
5244
|
-
summary: "Create a wiki page through the
|
|
5299
|
+
summary: "Create a wiki page through the SQLite-backed wiki surface",
|
|
5245
5300
|
responses: {
|
|
5246
5301
|
"200": jsonResponse({
|
|
5247
5302
|
type: "object",
|
|
@@ -5269,7 +5324,7 @@ export function buildOpenApiDocument() {
|
|
|
5269
5324
|
}
|
|
5270
5325
|
},
|
|
5271
5326
|
patch: {
|
|
5272
|
-
summary: "Update an existing wiki page through the
|
|
5327
|
+
summary: "Update an existing wiki page through the SQLite-backed surface",
|
|
5273
5328
|
responses: {
|
|
5274
5329
|
"200": jsonResponse({
|
|
5275
5330
|
type: "object",
|
|
@@ -5315,7 +5370,7 @@ export function buildOpenApiDocument() {
|
|
|
5315
5370
|
},
|
|
5316
5371
|
"/api/v1/wiki/sync": {
|
|
5317
5372
|
post: {
|
|
5318
|
-
summary: "
|
|
5373
|
+
summary: "Rebuild SQLite wiki search, link, and metadata indexes",
|
|
5319
5374
|
responses: {
|
|
5320
5375
|
"200": jsonResponse({
|
|
5321
5376
|
type: "object",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { getDatabase, runInTransaction } from "../db.js";
|
|
3
3
|
import { recordActivityEvent } from "./activity-events.js";
|
|
4
4
|
import { listAgentActions } from "./collaboration.js";
|
|
5
|
+
import { ensureBotUser, getUserById } from "./users.js";
|
|
5
6
|
import { agentActionSchema, agentRuntimeEventLevelSchema, agentRuntimeReconnectPlanSchema, agentRuntimeSessionEventSchema, agentRuntimeSessionSchema, createAgentRuntimeSessionEventSchema, createAgentRuntimeSessionSchema, disconnectAgentRuntimeSessionSchema, heartbeatAgentRuntimeSessionSchema, reconnectAgentRuntimeSessionSchema } from "../types.js";
|
|
6
7
|
function parseMetadata(raw) {
|
|
7
8
|
try {
|
|
@@ -211,7 +212,95 @@ function ensureCurrentSessionInstance(row, externalSessionId) {
|
|
|
211
212
|
}
|
|
212
213
|
return true;
|
|
213
214
|
}
|
|
214
|
-
function
|
|
215
|
+
function normalizeIdentityPart(value) {
|
|
216
|
+
return (value
|
|
217
|
+
?.trim()
|
|
218
|
+
.toLowerCase()
|
|
219
|
+
.replace(/[^a-z0-9._:]+/g, "_")
|
|
220
|
+
.replace(/^_+|_+$/g, "") ?? "");
|
|
221
|
+
}
|
|
222
|
+
function shortHash(value) {
|
|
223
|
+
return createHash("sha1").update(value).digest("hex").slice(0, 12);
|
|
224
|
+
}
|
|
225
|
+
function canonicalRuntimeAgentLabel(provider) {
|
|
226
|
+
if (provider === "openclaw") {
|
|
227
|
+
return "Forge OpenClaw";
|
|
228
|
+
}
|
|
229
|
+
if (provider === "hermes") {
|
|
230
|
+
return "Forge Hermes";
|
|
231
|
+
}
|
|
232
|
+
return "Forge Codex";
|
|
233
|
+
}
|
|
234
|
+
function canonicalRuntimeDescription(provider) {
|
|
235
|
+
return `${canonicalRuntimeAgentLabel(provider)} runtime agent with stable Forge identity and linked Kanban user.`;
|
|
236
|
+
}
|
|
237
|
+
function canonicalAgentUserSpec(provider) {
|
|
238
|
+
if (provider === "openclaw") {
|
|
239
|
+
return {
|
|
240
|
+
id: "user_agent_openclaw",
|
|
241
|
+
handle: "openclaw",
|
|
242
|
+
displayName: "OpenClaw",
|
|
243
|
+
description: "OpenClaw runtime actor linked to Forge agent identity and Kanban ownership.",
|
|
244
|
+
accentColor: "#38bdf8"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (provider === "hermes") {
|
|
248
|
+
return {
|
|
249
|
+
id: "user_agent_hermes",
|
|
250
|
+
handle: "hermes",
|
|
251
|
+
displayName: "Hermes",
|
|
252
|
+
description: "Hermes runtime actor linked to Forge agent identity and Kanban ownership.",
|
|
253
|
+
accentColor: "#a78bfa"
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
id: "user_agent_codex",
|
|
258
|
+
handle: "codex",
|
|
259
|
+
displayName: "Codex",
|
|
260
|
+
description: "Codex runtime actor linked to Forge agent identity and Kanban ownership.",
|
|
261
|
+
accentColor: "#22c55e"
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function deriveMachineKey(input) {
|
|
265
|
+
const explicit = normalizeIdentityPart(input.machineKey);
|
|
266
|
+
if (explicit) {
|
|
267
|
+
return explicit;
|
|
268
|
+
}
|
|
269
|
+
const source = [
|
|
270
|
+
normalizeText(input.dataRoot) ?? "",
|
|
271
|
+
normalizeText(input.baseUrl) ?? "local"
|
|
272
|
+
].join("|");
|
|
273
|
+
return `machine_${shortHash(source)}`;
|
|
274
|
+
}
|
|
275
|
+
function derivePersonaKey(input) {
|
|
276
|
+
return (normalizeIdentityPart(input.personaKey) ||
|
|
277
|
+
normalizeIdentityPart(input.agentType) ||
|
|
278
|
+
"default");
|
|
279
|
+
}
|
|
280
|
+
function deriveAgentIdentityKey(input) {
|
|
281
|
+
const explicit = normalizeIdentityPart(input.agentIdentityKey);
|
|
282
|
+
if (explicit) {
|
|
283
|
+
return explicit;
|
|
284
|
+
}
|
|
285
|
+
return `runtime:${input.provider}:${deriveMachineKey(input)}:${derivePersonaKey(input)}`;
|
|
286
|
+
}
|
|
287
|
+
function linkAgentIdentityUsers(agentId, provider, linkedUserIds, now) {
|
|
288
|
+
const primaryUser = ensureBotUser(canonicalAgentUserSpec(provider));
|
|
289
|
+
const normalizedUserIds = Array.from(new Set([primaryUser.id, ...linkedUserIds.map((id) => id.trim()).filter(Boolean)]));
|
|
290
|
+
for (const userId of normalizedUserIds) {
|
|
291
|
+
if (!getUserById(userId)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
getDatabase()
|
|
295
|
+
.prepare(`INSERT INTO agent_identity_users (agent_id, user_id, role, created_at, updated_at)
|
|
296
|
+
VALUES (?, ?, ?, ?, ?)
|
|
297
|
+
ON CONFLICT(agent_id, user_id) DO UPDATE SET
|
|
298
|
+
role = excluded.role,
|
|
299
|
+
updated_at = excluded.updated_at`)
|
|
300
|
+
.run(agentId, userId, userId === primaryUser.id ? "primary" : "linked", now, now);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function disconnectSupersededSingletonSessions(parsed, sessionId, agentId, now) {
|
|
215
304
|
if (!parsed.metadata?.singleton) {
|
|
216
305
|
return;
|
|
217
306
|
}
|
|
@@ -224,11 +313,11 @@ function disconnectSupersededSingletonSessions(parsed, sessionId, now) {
|
|
|
224
313
|
created_at, updated_at
|
|
225
314
|
FROM agent_runtime_sessions
|
|
226
315
|
WHERE provider = ?
|
|
227
|
-
AND
|
|
316
|
+
AND agent_id = ?
|
|
228
317
|
AND coalesce(base_url, '') = coalesce(?, '')
|
|
229
318
|
AND coalesce(data_root, '') = coalesce(?, '')
|
|
230
319
|
AND id <> ?`)
|
|
231
|
-
.all(parsed.provider,
|
|
320
|
+
.all(parsed.provider, agentId, normalizeText(parsed.baseUrl), normalizeText(parsed.dataRoot), sessionId);
|
|
232
321
|
for (const row of rows) {
|
|
233
322
|
if (row.status === "disconnected" && row.ended_at) {
|
|
234
323
|
continue;
|
|
@@ -251,29 +340,45 @@ function disconnectSupersededSingletonSessions(parsed, sessionId, now) {
|
|
|
251
340
|
}
|
|
252
341
|
}
|
|
253
342
|
function upsertRuntimeAgentIdentity(input) {
|
|
343
|
+
const identityKey = deriveAgentIdentityKey(input);
|
|
344
|
+
const machineKey = deriveMachineKey(input);
|
|
345
|
+
const personaKey = derivePersonaKey(input);
|
|
346
|
+
const label = canonicalRuntimeAgentLabel(input.provider);
|
|
254
347
|
const existing = getDatabase()
|
|
255
348
|
.prepare(`SELECT id
|
|
256
349
|
FROM agent_identities
|
|
257
|
-
WHERE
|
|
350
|
+
WHERE identity_key = ?
|
|
351
|
+
OR (
|
|
352
|
+
(identity_key IS NULL OR machine_key IS NULL OR machine_key = 'legacy' OR identity_key LIKE 'runtime:%:legacy:%')
|
|
353
|
+
AND (
|
|
354
|
+
provider = ?
|
|
355
|
+
OR lower(agent_type) = lower(?)
|
|
356
|
+
OR lower(label) = lower(?)
|
|
357
|
+
)
|
|
358
|
+
)
|
|
258
359
|
LIMIT 1`)
|
|
259
|
-
.get(input.
|
|
360
|
+
.get(identityKey, input.provider, input.provider, label);
|
|
260
361
|
const now = new Date().toISOString();
|
|
261
|
-
const description =
|
|
362
|
+
const description = canonicalRuntimeDescription(input.provider);
|
|
262
363
|
if (existing) {
|
|
263
364
|
getDatabase()
|
|
264
365
|
.prepare(`UPDATE agent_identities
|
|
265
|
-
SET agent_type = ?,
|
|
366
|
+
SET label = ?, agent_type = ?, identity_key = ?, provider = ?,
|
|
367
|
+
machine_key = ?, persona_key = ?, description = ?, updated_at = ?
|
|
266
368
|
WHERE id = ?`)
|
|
267
|
-
.run(input.agentType || input.provider, now, existing.id);
|
|
369
|
+
.run(label, input.agentType || input.provider, identityKey, input.provider, machineKey, personaKey, description, now, existing.id);
|
|
370
|
+
linkAgentIdentityUsers(existing.id, input.provider, input.linkedUserIds, now);
|
|
268
371
|
return existing.id;
|
|
269
372
|
}
|
|
270
373
|
const agentId = `agt_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
271
374
|
getDatabase()
|
|
272
375
|
.prepare(`INSERT INTO agent_identities (
|
|
273
|
-
id, label, agent_type,
|
|
376
|
+
id, label, agent_type, identity_key, provider, machine_key, persona_key,
|
|
377
|
+
trust_level, autonomy_mode, approval_mode,
|
|
274
378
|
description, created_at, updated_at
|
|
275
|
-
) VALUES (?, ?, ?, 'trusted', 'approval_required', 'approval_by_default', ?, ?, ?)`)
|
|
276
|
-
.run(agentId,
|
|
379
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'trusted', 'approval_required', 'approval_by_default', ?, ?, ?)`)
|
|
380
|
+
.run(agentId, label, input.agentType || input.provider, identityKey, input.provider, machineKey, personaKey, description, now, now);
|
|
381
|
+
linkAgentIdentityUsers(agentId, input.provider, input.linkedUserIds, now);
|
|
277
382
|
return agentId;
|
|
278
383
|
}
|
|
279
384
|
function insertSessionEvent(sessionId, input, now = new Date().toISOString()) {
|
|
@@ -321,6 +426,7 @@ export function registerAgentRuntimeSession(input) {
|
|
|
321
426
|
return runInTransaction(() => {
|
|
322
427
|
const now = new Date().toISOString();
|
|
323
428
|
const agentId = upsertRuntimeAgentIdentity(parsed);
|
|
429
|
+
const agentLabel = canonicalRuntimeAgentLabel(parsed.provider);
|
|
324
430
|
const existing = getSessionRowByCompositeKey(parsed.provider, parsed.sessionKey);
|
|
325
431
|
const sessionId = existing?.id ?? `ags_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
326
432
|
if (existing) {
|
|
@@ -332,7 +438,7 @@ export function registerAgentRuntimeSession(input) {
|
|
|
332
438
|
last_error = ?, last_seen_at = ?, last_heartbeat_at = ?, started_at = ?,
|
|
333
439
|
ended_at = NULL, metadata_json = ?, updated_at = ?
|
|
334
440
|
WHERE id = ?`)
|
|
335
|
-
.run(agentId,
|
|
441
|
+
.run(agentId, agentLabel, parsed.agentType || parsed.provider, parsed.sessionLabel || parsed.sessionKey, parsed.actorLabel, parsed.connectionMode, parsed.status === "error" ? "error" : "connected", normalizeText(parsed.baseUrl), normalizeText(parsed.webUrl), normalizeText(parsed.dataRoot), normalizeText(parsed.externalSessionId), parsed.staleAfterSeconds, normalizeText(parsed.lastError), now, now, now, JSON.stringify(parsed.metadata), now, sessionId);
|
|
336
442
|
insertSessionEvent(sessionId, {
|
|
337
443
|
eventType: "session_registered",
|
|
338
444
|
title: "Session re-registered",
|
|
@@ -349,7 +455,7 @@ export function registerAgentRuntimeSession(input) {
|
|
|
349
455
|
last_seen_at, last_heartbeat_at, started_at, ended_at, metadata_json,
|
|
350
456
|
created_at, updated_at
|
|
351
457
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?, ?, ?, NULL, ?, ?, ?)`)
|
|
352
|
-
.run(sessionId, agentId,
|
|
458
|
+
.run(sessionId, agentId, agentLabel, parsed.agentType || parsed.provider, parsed.provider, parsed.sessionKey, parsed.sessionLabel || parsed.sessionKey, parsed.actorLabel, parsed.connectionMode, parsed.status === "error" ? "error" : "connected", normalizeText(parsed.baseUrl), normalizeText(parsed.webUrl), normalizeText(parsed.dataRoot), normalizeText(parsed.externalSessionId), parsed.staleAfterSeconds, normalizeText(parsed.lastError), now, now, now, JSON.stringify(parsed.metadata), now, now);
|
|
353
459
|
insertSessionEvent(sessionId, {
|
|
354
460
|
eventType: "session_registered",
|
|
355
461
|
title: "Session registered",
|
|
@@ -357,12 +463,12 @@ export function registerAgentRuntimeSession(input) {
|
|
|
357
463
|
metadata: parsed.metadata
|
|
358
464
|
}, now);
|
|
359
465
|
}
|
|
360
|
-
disconnectSupersededSingletonSessions(parsed, sessionId, now);
|
|
466
|
+
disconnectSupersededSingletonSessions(parsed, sessionId, agentId, now);
|
|
361
467
|
recordActivityEvent({
|
|
362
468
|
entityType: "session",
|
|
363
469
|
entityId: sessionId,
|
|
364
470
|
eventType: "agent_session_registered",
|
|
365
|
-
title: `Agent session registered: ${
|
|
471
|
+
title: `Agent session registered: ${agentLabel}`,
|
|
366
472
|
description: `${parsed.provider} registered a live agent session.`,
|
|
367
473
|
actor: parsed.actorLabel,
|
|
368
474
|
source: "agent",
|
|
@@ -47,6 +47,11 @@ export function buildConnectionAgentIdentity(connection) {
|
|
|
47
47
|
id: connection.agentId,
|
|
48
48
|
label: connection.agentLabel,
|
|
49
49
|
agentType: connection.provider,
|
|
50
|
+
identityKey: `model:${connection.id}`,
|
|
51
|
+
provider: null,
|
|
52
|
+
machineKey: null,
|
|
53
|
+
personaKey: connection.provider,
|
|
54
|
+
linkedUsers: [],
|
|
50
55
|
trustLevel: "trusted",
|
|
51
56
|
autonomyMode: "approval_required",
|
|
52
57
|
approvalMode: "approval_by_default",
|
|
@@ -40,6 +40,9 @@ function normalizeTags(tags) {
|
|
|
40
40
|
return true;
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
+
function canonicalNoteSourcePath() {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
43
46
|
function parseTagsJson(raw) {
|
|
44
47
|
try {
|
|
45
48
|
const parsed = JSON.parse(raw);
|
|
@@ -438,7 +441,7 @@ export function createNote(input, context) {
|
|
|
438
441
|
source_path, frontmatter_json, revision_hash, last_synced_at, created_at, updated_at
|
|
439
442
|
)
|
|
440
443
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
441
|
-
.run(id, wikiFields.kind, wikiFields.title, wikiFields.slug, wikiFields.spaceId, wikiFields.parentSlug, wikiFields.indexOrder, wikiFields.showInIndex ? 1 : 0, JSON.stringify(wikiFields.aliases), wikiFields.summary, parsed.contentMarkdown, contentPlain, parsed.author ?? context.actor ?? null, context.source, JSON.stringify(parsed.tags), parsed.destroyAt,
|
|
444
|
+
.run(id, wikiFields.kind, wikiFields.title, wikiFields.slug, wikiFields.spaceId, wikiFields.parentSlug, wikiFields.indexOrder, wikiFields.showInIndex ? 1 : 0, JSON.stringify(wikiFields.aliases), wikiFields.summary, parsed.contentMarkdown, contentPlain, parsed.author ?? context.actor ?? null, context.source, JSON.stringify(parsed.tags), parsed.destroyAt, canonicalNoteSourcePath(), JSON.stringify(parsed.frontmatter), parsed.revisionHash, parsed.lastSyncedAt ?? null, now, now);
|
|
442
445
|
insertLinks(id, parsed.links, now);
|
|
443
446
|
setEntityOwner("note", id, parsed.userId, parsed.author ?? context.actor ?? null);
|
|
444
447
|
clearDeletedEntityRecord("note", id);
|
|
@@ -504,7 +507,7 @@ export function updateNote(noteId, input, context) {
|
|
|
504
507
|
existing
|
|
505
508
|
});
|
|
506
509
|
const nextFrontmatter = patch.frontmatter === undefined ? existing.frontmatter : patch.frontmatter;
|
|
507
|
-
const nextSourcePath =
|
|
510
|
+
const nextSourcePath = canonicalNoteSourcePath();
|
|
508
511
|
const nextRevisionHash = patch.revisionHash === undefined
|
|
509
512
|
? existing.revisionHash
|
|
510
513
|
: patch.revisionHash;
|