@gmickel/gno 2.5.1 → 2.7.0
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 +60 -5
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +80 -4
- package/assets/skill/cli-reference.md +132 -2
- package/assets/skill/examples.md +30 -0
- package/assets/skill/mcp-reference.md +54 -1
- package/assets/skill/recipes/capture-and-file.md +6 -0
- package/assets/skill/recipes/memory-file-decision.md +6 -0
- package/assets/skill/recipes/memory-supersede-fact.md +5 -0
- package/assets/skill/recipes/session-evidence-lookup.md +98 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.5.1.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +449 -35
- package/spec/mcp.md +234 -4
- package/spec/output-schemas/ask.schema.json +1 -1
- package/spec/output-schemas/capture-receipt.schema.json +4 -1
- package/spec/output-schemas/doctor.schema.json +88 -0
- package/spec/output-schemas/error.schema.json +11 -2
- package/spec/output-schemas/get.schema.json +1 -1
- package/spec/output-schemas/mcp-capture-result.schema.json +4 -2
- package/spec/output-schemas/memory-remember.schema.json +10 -4
- package/spec/output-schemas/multi-get.schema.json +4 -1
- package/spec/output-schemas/peek.schema.json +2 -9
- package/spec/output-schemas/request-status.schema.json +113 -0
- package/spec/output-schemas/resident-status.schema.json +22 -0
- package/spec/output-schemas/search-result.schema.json +1 -1
- package/spec/output-schemas/search-results.schema.json +1 -1
- package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
- package/spec/output-schemas/sessions-discovery.schema.json +38 -0
- package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
- package/spec/output-schemas/sessions-status.schema.json +432 -0
- package/spec/output-schemas/status.schema.json +98 -0
- package/src/cli/commands/ask.ts +14 -2
- package/src/cli/commands/capture.ts +55 -96
- package/src/cli/commands/daemon.ts +41 -0
- package/src/cli/commands/doctor.ts +54 -20
- package/src/cli/commands/embed.ts +41 -3
- package/src/cli/commands/ls.ts +3 -0
- package/src/cli/commands/memory.ts +12 -3
- package/src/cli/commands/query.ts +5 -0
- package/src/cli/commands/request-status.ts +59 -0
- package/src/cli/commands/reset.ts +39 -5
- package/src/cli/commands/sessions.ts +713 -0
- package/src/cli/commands/shared.ts +14 -1
- package/src/cli/commands/status.ts +63 -5
- package/src/cli/commands/vec.ts +54 -0
- package/src/cli/detach.ts +29 -1
- package/src/cli/errors.ts +13 -9
- package/src/cli/program.ts +441 -2
- package/src/cli/session-binding.ts +49 -0
- package/src/config/types.ts +8 -0
- package/src/core/capture-publish.ts +239 -0
- package/src/core/capture-sync.ts +12 -2
- package/src/core/host-paths.ts +31 -0
- package/src/core/memory-remember.ts +234 -122
- package/src/core/memory-types.ts +11 -0
- package/src/core/network-boundary-inventory.ts +8 -0
- package/src/core/request-receipts.ts +671 -0
- package/src/core/shutdown-budget.ts +6 -0
- package/src/core/vector-partition-status.ts +52 -0
- package/src/embed/backlog.ts +124 -18
- package/src/embed/fingerprint.ts +6 -3
- package/src/embed/retry.ts +66 -27
- package/src/embed/variant-backlog.ts +15 -10
- package/src/embed/variant-retry.ts +31 -22
- package/src/index.ts +30 -2
- package/src/llm/native-worker/dispatcher.ts +2 -0
- package/src/llm/native-worker/embedding-identity.ts +42 -0
- package/src/llm/native-worker/protocol.ts +1 -0
- package/src/llm/types.ts +3 -0
- package/src/mcp/context.ts +17 -0
- package/src/mcp/http-egress.ts +4 -0
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/resources/index.ts +6 -5
- package/src/mcp/tool-descriptions-core.ts +1 -1
- package/src/mcp/tools/capture.ts +87 -85
- package/src/mcp/tools/index.ts +77 -4
- package/src/mcp/tools/memory-remember.ts +8 -1
- package/src/mcp/tools/memory-shared.ts +7 -1
- package/src/mcp/tools/request-status.ts +73 -0
- package/src/mcp/tools/sessions.ts +208 -0
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/hybrid.ts +37 -7
- package/src/pipeline/vsearch.ts +14 -2
- package/src/sdk/client.ts +180 -84
- package/src/sdk/index.ts +6 -0
- package/src/sdk/types.ts +54 -2
- package/src/serve/capture-service.ts +98 -32
- package/src/serve/config-sync.ts +3 -2
- package/src/serve/embed-scheduler.ts +133 -19
- package/src/serve/host-path-redaction.ts +79 -0
- package/src/serve/public/app.tsx +4 -1
- package/src/serve/public/components/CaptureModal.tsx +26 -8
- package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
- package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
- package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
- package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
- package/src/serve/public/components/sessions/api.ts +40 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +26 -3
- package/src/serve/public/lib/request-intent.ts +77 -0
- package/src/serve/public/lib/snippet.tsx +52 -0
- package/src/serve/public/lib/workspace-actions.ts +12 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Dashboard.tsx +22 -9
- package/src/serve/public/pages/DocView.tsx +25 -6
- package/src/serve/public/pages/DocumentEditor.tsx +224 -104
- package/src/serve/public/pages/Search.tsx +1 -41
- package/src/serve/public/pages/Sessions.tsx +350 -0
- package/src/serve/resident-runtime.ts +69 -4
- package/src/serve/resident-status.ts +13 -1
- package/src/serve/routes/api.ts +476 -147
- package/src/serve/routes/sessions.ts +766 -0
- package/src/serve/security.ts +9 -0
- package/src/serve/server.ts +215 -10
- package/src/serve/session-automation.ts +146 -0
- package/src/serve/status-model.ts +16 -0
- package/src/serve/status.ts +2 -0
- package/src/serve/watch-reconciliation-shared.ts +3 -0
- package/src/serve/watch-service-events.ts +3 -2
- package/src/serve/watch-service-run-flush.ts +35 -2
- package/src/serve/watch-service.ts +5 -0
- package/src/sessions/archive.ts +348 -0
- package/src/sessions/automation-state.ts +444 -0
- package/src/sessions/automation-status.ts +239 -0
- package/src/sessions/automation.ts +1169 -0
- package/src/sessions/binding.ts +105 -0
- package/src/sessions/claude-hook.ts +240 -0
- package/src/sessions/config.ts +176 -0
- package/src/sessions/format.ts +191 -0
- package/src/sessions/import-child-env.ts +8 -0
- package/src/sessions/import-child.ts +152 -0
- package/src/sessions/parsers/claude-code.ts +259 -0
- package/src/sessions/parsers/codex.ts +303 -0
- package/src/sessions/parsers/hermes.ts +248 -0
- package/src/sessions/parsers/openclaw.ts +496 -0
- package/src/sessions/parsers/shared.ts +184 -0
- package/src/sessions/sanitize.ts +222 -0
- package/src/sessions/service.ts +1533 -0
- package/src/sessions/setup.ts +477 -0
- package/src/sessions/sources.ts +518 -0
- package/src/sessions/state.ts +118 -0
- package/src/sessions/types.ts +457 -0
- package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
- package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +76 -16
- package/src/store/sqlite/scoped-index.ts +9 -0
- package/src/store/types.ts +11 -1
- package/src/store/vector/lazy.ts +46 -43
- package/src/store/vector/runtime-compat.ts +651 -0
- package/src/store/vector/sqlite-vec.ts +20 -2
- package/src/store/vector/status.ts +276 -35
- package/src/store/vector/types.ts +2 -0
- package/src/store/vector/variant-search.ts +71 -23
- package/src/store/vector/variants.ts +49 -14
- package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +0 -1
package/src/serve/routes/api.ts
CHANGED
|
@@ -68,6 +68,8 @@ import {
|
|
|
68
68
|
getDocumentCapabilities,
|
|
69
69
|
} from "../../core/document-capabilities";
|
|
70
70
|
import { EgressAuditService } from "../../core/egress-audit";
|
|
71
|
+
import { MCP_ERRORS } from "../../core/errors";
|
|
72
|
+
import { withWriteLock } from "../../core/file-lock";
|
|
71
73
|
import {
|
|
72
74
|
atomicWrite,
|
|
73
75
|
copyFilePath,
|
|
@@ -109,6 +111,18 @@ import {
|
|
|
109
111
|
resolveRemoteProjectAffinity,
|
|
110
112
|
} from "../../core/project-affinity-surface";
|
|
111
113
|
import { projectRecordEvidenceMetadata } from "../../core/record-metadata";
|
|
114
|
+
import {
|
|
115
|
+
isLeaseBusy,
|
|
116
|
+
localRequestLedger,
|
|
117
|
+
readRequestStatus,
|
|
118
|
+
REQUEST_RECEIPT_HTTP_STATUS,
|
|
119
|
+
type RequestCheckpoint,
|
|
120
|
+
RequestReceiptError,
|
|
121
|
+
type RequestReceiptInfo,
|
|
122
|
+
requestDigest,
|
|
123
|
+
runRequestedWrite,
|
|
124
|
+
validateRequestId,
|
|
125
|
+
} from "../../core/request-receipts";
|
|
112
126
|
import {
|
|
113
127
|
retrievalTraceFilters,
|
|
114
128
|
startRetrievalTraceRequest,
|
|
@@ -130,7 +144,7 @@ import {
|
|
|
130
144
|
type MetadataPredicate,
|
|
131
145
|
} from "../../core/typed-metadata";
|
|
132
146
|
import { validateRelPath } from "../../core/validation";
|
|
133
|
-
import { writeLeasePath } from "../../core/write-lease";
|
|
147
|
+
import { DEFAULT_LOCK_WAIT_MS, writeLeasePath } from "../../core/write-lease";
|
|
134
148
|
import {
|
|
135
149
|
defaultSyncService,
|
|
136
150
|
type SyncResult,
|
|
@@ -163,8 +177,10 @@ import { exportPublishArtifact } from "../../publish/export-service";
|
|
|
163
177
|
import { buildBrowseTree, normalizeBrowsePath } from "../browse-tree";
|
|
164
178
|
import {
|
|
165
179
|
classifyResidentCaptureError,
|
|
180
|
+
executeRequestedResidentCapture,
|
|
166
181
|
executeResidentCapturePlan,
|
|
167
182
|
planResidentCapture,
|
|
183
|
+
ResidentCapturePlanError,
|
|
168
184
|
type ResidentCaptureDependencies,
|
|
169
185
|
} from "../capture-service";
|
|
170
186
|
import { parseClosedJson } from "../closed-json";
|
|
@@ -493,7 +509,10 @@ export interface CreateDocRequestBody {
|
|
|
493
509
|
tags?: string[];
|
|
494
510
|
}
|
|
495
511
|
|
|
496
|
-
export interface CreateCaptureRequestBody extends PublicCaptureInput {
|
|
512
|
+
export interface CreateCaptureRequestBody extends PublicCaptureInput {
|
|
513
|
+
/** Opt-in retry identity (see docs/API.md "Request IDs"). */
|
|
514
|
+
requestId?: string;
|
|
515
|
+
}
|
|
497
516
|
|
|
498
517
|
export interface RenameDocRequestBody {
|
|
499
518
|
name: string;
|
|
@@ -546,6 +565,8 @@ export interface UpdateDocRequestBody {
|
|
|
546
565
|
expectedModifiedAt?: string;
|
|
547
566
|
/** Exact document URI when docid is not unique across duplicate content */
|
|
548
567
|
uri?: string;
|
|
568
|
+
/** Opt-in retry identity (see docs/API.md "Request IDs"). */
|
|
569
|
+
requestId?: string;
|
|
549
570
|
}
|
|
550
571
|
|
|
551
572
|
export interface CreateEditableCopyRequestBody {
|
|
@@ -595,6 +616,16 @@ function errorResponse(
|
|
|
595
616
|
);
|
|
596
617
|
}
|
|
597
618
|
|
|
619
|
+
/** Request receipt errors keep their stable code; null for anything else. */
|
|
620
|
+
function requestErrorResponse(error: unknown): Response | null {
|
|
621
|
+
if (!(error instanceof RequestReceiptError)) return null;
|
|
622
|
+
return errorResponse(
|
|
623
|
+
error.code,
|
|
624
|
+
error.message,
|
|
625
|
+
REQUEST_RECEIPT_HTTP_STATUS[error.code]
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
598
629
|
function parseRestMetadataFilter(
|
|
599
630
|
value: unknown
|
|
600
631
|
):
|
|
@@ -3295,9 +3326,87 @@ export async function handleRevealDoc(
|
|
|
3295
3326
|
}
|
|
3296
3327
|
}
|
|
3297
3328
|
|
|
3329
|
+
/**
|
|
3330
|
+
* GET /api/requests/:requestId
|
|
3331
|
+
* Local-owner lookup of a capture/remember/document-update request ID.
|
|
3332
|
+
* Returns a content-free pointer, never the stored outcome.
|
|
3333
|
+
*/
|
|
3334
|
+
export async function handleRequestStatus(
|
|
3335
|
+
store: SqliteAdapter,
|
|
3336
|
+
requestId: string
|
|
3337
|
+
): Promise<Response> {
|
|
3338
|
+
try {
|
|
3339
|
+
return jsonResponse(
|
|
3340
|
+
await readRequestStatus({
|
|
3341
|
+
...localRequestLedger(store.getDbPath()),
|
|
3342
|
+
requestId,
|
|
3343
|
+
})
|
|
3344
|
+
);
|
|
3345
|
+
} catch (error) {
|
|
3346
|
+
return (
|
|
3347
|
+
requestErrorResponse(error) ??
|
|
3348
|
+
errorResponse(
|
|
3349
|
+
"RUNTIME",
|
|
3350
|
+
error instanceof Error ? error.message : String(error),
|
|
3351
|
+
500
|
|
3352
|
+
)
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
/** A document update refused before anything was written. */
|
|
3358
|
+
class DocUpdateRejected extends Error {
|
|
3359
|
+
readonly response: Response;
|
|
3360
|
+
|
|
3361
|
+
constructor(response: Response) {
|
|
3362
|
+
super("document update rejected");
|
|
3363
|
+
this.response = response;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
/** Private recovery record for one admitted document update. */
|
|
3368
|
+
interface DocUpdatePlan {
|
|
3369
|
+
fullPath: string;
|
|
3370
|
+
collection: string;
|
|
3371
|
+
relPath: string;
|
|
3372
|
+
docid: string;
|
|
3373
|
+
/** Stable identity; row IDs do not survive an index rebuild. */
|
|
3374
|
+
gnoUri: string;
|
|
3375
|
+
baseHash: string;
|
|
3376
|
+
/** Tag set before and after this update (null when tags are unchanged). */
|
|
3377
|
+
baseTagState: string | null;
|
|
3378
|
+
resultTagState: string | null;
|
|
3379
|
+
/** User-source tags before this update: store-only tag writes change them. */
|
|
3380
|
+
baseUserTagState: string | null;
|
|
3381
|
+
/** Null when only store tags change (no file write). */
|
|
3382
|
+
newHash: string | null;
|
|
3383
|
+
indexedSourceHash: string;
|
|
3384
|
+
indexedModifiedAt: string;
|
|
3385
|
+
writeBack?: "applied" | "skipped_unsupported";
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
/** Retained outcome: the response body plus what deferred sync needs. */
|
|
3389
|
+
interface DocUpdateOutcome {
|
|
3390
|
+
body: {
|
|
3391
|
+
success: true;
|
|
3392
|
+
docId: string;
|
|
3393
|
+
uri: string;
|
|
3394
|
+
path: string;
|
|
3395
|
+
jobId: string | null;
|
|
3396
|
+
writeBack?: "applied" | "skipped_unsupported";
|
|
3397
|
+
version: { sourceHash: string; modifiedAt: string };
|
|
3398
|
+
};
|
|
3399
|
+
target: { gnoUri: string; collection: string; relPath: string };
|
|
3400
|
+
/** The file changed, so lexical sync is deferred work. */
|
|
3401
|
+
contentWritten: boolean;
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3298
3404
|
/**
|
|
3299
3405
|
* PUT /api/docs/:id
|
|
3300
3406
|
* Update an existing document's content and/or tags.
|
|
3407
|
+
*
|
|
3408
|
+
* The revision check and file publication run under the shared write lease
|
|
3409
|
+
* (with or without a request ID). Lexical sync and embedding stay deferred.
|
|
3301
3410
|
*/
|
|
3302
3411
|
export async function handleUpdateDoc(
|
|
3303
3412
|
ctxHolder: ContextHolder,
|
|
@@ -3306,6 +3415,10 @@ export async function handleUpdateDoc(
|
|
|
3306
3415
|
req: Request,
|
|
3307
3416
|
deps?: {
|
|
3308
3417
|
syncCollection?: typeof defaultSyncService.syncCollection;
|
|
3418
|
+
/** Shared `.mcp-write.lock` path; defaults to the resident index's lease. */
|
|
3419
|
+
lockPath?: string;
|
|
3420
|
+
lockWaitMs?: number;
|
|
3421
|
+
requestCheckpoint?: (stage: RequestCheckpoint) => Promise<void> | void;
|
|
3309
3422
|
}
|
|
3310
3423
|
): Promise<Response> {
|
|
3311
3424
|
let body: UpdateDocRequestBody;
|
|
@@ -3342,6 +3455,15 @@ export async function handleUpdateDoc(
|
|
|
3342
3455
|
if (body.uri !== undefined && typeof body.uri !== "string") {
|
|
3343
3456
|
return errorResponse("VALIDATION", "uri must be a string");
|
|
3344
3457
|
}
|
|
3458
|
+
let requestId: string | undefined;
|
|
3459
|
+
try {
|
|
3460
|
+
requestId =
|
|
3461
|
+
body.requestId === undefined
|
|
3462
|
+
? undefined
|
|
3463
|
+
: validateRequestId(body.requestId);
|
|
3464
|
+
} catch (error) {
|
|
3465
|
+
return requestErrorResponse(error) as Response;
|
|
3466
|
+
}
|
|
3345
3467
|
|
|
3346
3468
|
// Validate tags if provided
|
|
3347
3469
|
let normalizedTags: string[] | undefined;
|
|
@@ -3365,176 +3487,349 @@ export async function handleUpdateDoc(
|
|
|
3365
3487
|
}
|
|
3366
3488
|
}
|
|
3367
3489
|
|
|
3368
|
-
|
|
3369
|
-
const
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
capabilities.reason ??
|
|
3383
|
-
"This document cannot be edited in place. Create an editable markdown copy instead.",
|
|
3384
|
-
409
|
|
3385
|
-
);
|
|
3386
|
-
}
|
|
3387
|
-
|
|
3388
|
-
const resolvedDocPath = await resolveAbsoluteDocPath(
|
|
3389
|
-
ctxHolder.config.collections,
|
|
3390
|
-
doc
|
|
3391
|
-
);
|
|
3392
|
-
if (!resolvedDocPath) {
|
|
3393
|
-
return errorResponse(
|
|
3394
|
-
"NOT_FOUND",
|
|
3395
|
-
`Collection not found: ${doc.collection}`,
|
|
3396
|
-
404
|
|
3397
|
-
);
|
|
3398
|
-
}
|
|
3399
|
-
const { collection, fullPath } = resolvedDocPath;
|
|
3490
|
+
/** Sorted tag set; with `userTags`, the set once those replace the user tags. */
|
|
3491
|
+
const tagState = async (
|
|
3492
|
+
documentId: number,
|
|
3493
|
+
userTags?: string[]
|
|
3494
|
+
): Promise<string> => {
|
|
3495
|
+
const rows = await store.getTagsForDoc(documentId);
|
|
3496
|
+
if (!rows.ok) throw new Error(rows.error.message);
|
|
3497
|
+
const kept = userTags
|
|
3498
|
+
? rows.value.filter((row) => row.source !== "user")
|
|
3499
|
+
: rows.value;
|
|
3500
|
+
return [...new Set([...kept.map((row) => row.tag), ...(userTags ?? [])])]
|
|
3501
|
+
.sort()
|
|
3502
|
+
.join("\n");
|
|
3503
|
+
};
|
|
3400
3504
|
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
return
|
|
3405
|
-
|
|
3505
|
+
const userTagState = async (documentId: number): Promise<string> => {
|
|
3506
|
+
const rows = await store.getTagsForDoc(documentId);
|
|
3507
|
+
if (!rows.ok) throw new Error(rows.error.message);
|
|
3508
|
+
return rows.value
|
|
3509
|
+
.filter((row) => row.source === "user")
|
|
3510
|
+
.map((row) => row.tag)
|
|
3511
|
+
.sort()
|
|
3512
|
+
.join("\n");
|
|
3513
|
+
};
|
|
3406
3514
|
|
|
3407
|
-
|
|
3408
|
-
const currentBytes = await file.bytes();
|
|
3409
|
-
const currentSourceHash = hashContent(
|
|
3410
|
-
new TextDecoder().decode(currentBytes)
|
|
3411
|
-
);
|
|
3515
|
+
const readModifiedAt = async (path: string): Promise<string> => {
|
|
3412
3516
|
const { stat } = await import("node:fs/promises"); // no Bun structure stat parity
|
|
3413
|
-
|
|
3517
|
+
return (await stat(path)).mtime.toISOString();
|
|
3518
|
+
};
|
|
3414
3519
|
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
message: "Document changed on disk. Reload before saving.",
|
|
3425
|
-
},
|
|
3426
|
-
currentVersion: {
|
|
3427
|
-
sourceHash: currentSourceHash,
|
|
3428
|
-
modifiedAt: currentModifiedAt,
|
|
3429
|
-
},
|
|
3430
|
-
},
|
|
3431
|
-
409
|
|
3520
|
+
/** Under the lease: resolve, revision-check, plan. Never writes. */
|
|
3521
|
+
const prepare = async (): Promise<{
|
|
3522
|
+
plan: DocUpdatePlan;
|
|
3523
|
+
publish: () => Promise<void>;
|
|
3524
|
+
}> => {
|
|
3525
|
+
const docResult = await resolveDocumentReference(store, docId, body.uri);
|
|
3526
|
+
if (!docResult.ok) {
|
|
3527
|
+
throw new DocUpdateRejected(
|
|
3528
|
+
errorResponse("RUNTIME", docResult.error.message, 500)
|
|
3432
3529
|
);
|
|
3433
3530
|
}
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3531
|
+
if (!docResult.value) {
|
|
3532
|
+
throw new DocUpdateRejected(
|
|
3533
|
+
errorResponse("NOT_FOUND", "Document not found", 404)
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
const doc = docResult.value;
|
|
3537
|
+
const capabilities = capabilitiesForDocument(doc);
|
|
3538
|
+
if (hasContent && !capabilities.editable) {
|
|
3539
|
+
throw new DocUpdateRejected(
|
|
3540
|
+
errorResponse(
|
|
3541
|
+
"READ_ONLY",
|
|
3542
|
+
capabilities.reason ??
|
|
3543
|
+
"This document cannot be edited in place. Create an editable markdown copy instead.",
|
|
3544
|
+
409
|
|
3545
|
+
)
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
const resolvedDocPath = await resolveAbsoluteDocPath(
|
|
3549
|
+
ctxHolder.config.collections,
|
|
3550
|
+
doc
|
|
3551
|
+
);
|
|
3552
|
+
if (!resolvedDocPath) {
|
|
3553
|
+
throw new DocUpdateRejected(
|
|
3554
|
+
errorResponse(
|
|
3555
|
+
"NOT_FOUND",
|
|
3556
|
+
`Collection not found: ${doc.collection}`,
|
|
3557
|
+
404
|
|
3558
|
+
)
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
const { fullPath } = resolvedDocPath;
|
|
3562
|
+
const file = Bun.file(fullPath);
|
|
3563
|
+
if (!(await file.exists())) {
|
|
3564
|
+
throw new DocUpdateRejected(
|
|
3565
|
+
errorResponse("FILE_NOT_FOUND", "Source file no longer exists", 404)
|
|
3566
|
+
);
|
|
3567
|
+
}
|
|
3568
|
+
const currentText = await file.text();
|
|
3569
|
+
const tagStates = normalizedTags
|
|
3570
|
+
? {
|
|
3571
|
+
base: await tagState(doc.id),
|
|
3572
|
+
result: await tagState(doc.id, normalizedTags),
|
|
3573
|
+
baseUser: await userTagState(doc.id),
|
|
3574
|
+
}
|
|
3575
|
+
: null;
|
|
3576
|
+
const currentSourceHash = hashContent(currentText);
|
|
3577
|
+
if (body.expectedSourceHash || body.expectedModifiedAt) {
|
|
3578
|
+
const currentModifiedAt = await readModifiedAt(fullPath);
|
|
3579
|
+
if (
|
|
3580
|
+
(body.expectedSourceHash &&
|
|
3581
|
+
body.expectedSourceHash !== currentSourceHash) ||
|
|
3582
|
+
(body.expectedModifiedAt &&
|
|
3583
|
+
body.expectedModifiedAt !== currentModifiedAt)
|
|
3584
|
+
) {
|
|
3585
|
+
throw new DocUpdateRejected(
|
|
3586
|
+
jsonResponse(
|
|
3587
|
+
{
|
|
3588
|
+
error: {
|
|
3589
|
+
code: "CONFLICT",
|
|
3590
|
+
message: "Document changed on disk. Reload before saving.",
|
|
3591
|
+
},
|
|
3592
|
+
currentVersion: {
|
|
3593
|
+
sourceHash: currentSourceHash,
|
|
3594
|
+
modifiedAt: currentModifiedAt,
|
|
3595
|
+
},
|
|
3596
|
+
},
|
|
3597
|
+
409
|
|
3598
|
+
)
|
|
3599
|
+
);
|
|
3600
|
+
}
|
|
3444
3601
|
}
|
|
3445
3602
|
|
|
3446
|
-
|
|
3447
|
-
|
|
3603
|
+
let contentToWrite = hasContent ? body.content : undefined;
|
|
3604
|
+
let writeBack: DocUpdatePlan["writeBack"];
|
|
3605
|
+
if (normalizedTags) {
|
|
3448
3606
|
if (capabilities.tagsWriteback) {
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3607
|
+
contentToWrite = updateFrontmatterTags(
|
|
3608
|
+
contentToWrite ?? currentText,
|
|
3609
|
+
normalizedTags
|
|
3610
|
+
);
|
|
3452
3611
|
writeBack = "applied";
|
|
3453
3612
|
} else {
|
|
3454
3613
|
writeBack = "skipped_unsupported";
|
|
3455
3614
|
}
|
|
3456
|
-
|
|
3457
|
-
// Update tags in DB (user source since this is a user action)
|
|
3458
|
-
const tagResult = await store.setDocTags(doc.id, normalizedTags, "user");
|
|
3459
|
-
if (!tagResult.ok) {
|
|
3460
|
-
return errorResponse("RUNTIME", tagResult.error.message, 500);
|
|
3461
|
-
}
|
|
3462
3615
|
}
|
|
3616
|
+
return {
|
|
3617
|
+
plan: {
|
|
3618
|
+
fullPath,
|
|
3619
|
+
collection: doc.collection,
|
|
3620
|
+
relPath: doc.relPath,
|
|
3621
|
+
baseTagState: tagStates?.base ?? null,
|
|
3622
|
+
resultTagState: tagStates?.result ?? null,
|
|
3623
|
+
baseUserTagState: tagStates?.baseUser ?? null,
|
|
3624
|
+
docid: doc.docid,
|
|
3625
|
+
gnoUri: doc.uri,
|
|
3626
|
+
baseHash: currentSourceHash,
|
|
3627
|
+
newHash:
|
|
3628
|
+
contentToWrite === undefined ? null : hashContent(contentToWrite),
|
|
3629
|
+
indexedSourceHash: doc.sourceHash,
|
|
3630
|
+
indexedModifiedAt: doc.sourceMtime,
|
|
3631
|
+
writeBack,
|
|
3632
|
+
},
|
|
3633
|
+
publish: async () => {
|
|
3634
|
+
if (contentToWrite === undefined) return;
|
|
3635
|
+
ctxHolder.watchService?.suppress(fullPath);
|
|
3636
|
+
await atomicWrite(fullPath, contentToWrite);
|
|
3637
|
+
},
|
|
3638
|
+
};
|
|
3639
|
+
};
|
|
3463
3640
|
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
const
|
|
3473
|
-
|
|
3641
|
+
/** Committed boundary: file published, tags stored, new hash recorded. */
|
|
3642
|
+
const finish = async (plan: DocUpdatePlan): Promise<DocUpdateOutcome> => {
|
|
3643
|
+
if (normalizedTags) {
|
|
3644
|
+
const current = await store.getDocumentByUri(plan.gnoUri);
|
|
3645
|
+
if (!current.ok || !current.value) {
|
|
3646
|
+
throw new Error(`${plan.gnoUri} is no longer indexed`);
|
|
3647
|
+
}
|
|
3648
|
+
// User source since this is a user action.
|
|
3649
|
+
const tagResult = await store.setDocTags(
|
|
3650
|
+
current.value.id,
|
|
3651
|
+
normalizedTags,
|
|
3652
|
+
"user"
|
|
3653
|
+
);
|
|
3654
|
+
if (!tagResult.ok) throw new Error(tagResult.error.message);
|
|
3474
3655
|
}
|
|
3475
|
-
|
|
3476
|
-
// Build proper file:// URI using node:url
|
|
3477
3656
|
const { pathToFileURL } = await import("node:url");
|
|
3478
|
-
|
|
3657
|
+
return {
|
|
3658
|
+
body: {
|
|
3659
|
+
success: true,
|
|
3660
|
+
docId: plan.docid,
|
|
3661
|
+
uri: pathToFileURL(plan.fullPath).href,
|
|
3662
|
+
path: plan.fullPath,
|
|
3663
|
+
jobId: null,
|
|
3664
|
+
writeBack: plan.writeBack,
|
|
3665
|
+
version:
|
|
3666
|
+
plan.newHash === null
|
|
3667
|
+
? {
|
|
3668
|
+
sourceHash: plan.indexedSourceHash,
|
|
3669
|
+
modifiedAt: plan.indexedModifiedAt,
|
|
3670
|
+
}
|
|
3671
|
+
: {
|
|
3672
|
+
sourceHash: plan.newHash,
|
|
3673
|
+
modifiedAt: await readModifiedAt(plan.fullPath),
|
|
3674
|
+
},
|
|
3675
|
+
},
|
|
3676
|
+
target: {
|
|
3677
|
+
gnoUri: plan.gnoUri,
|
|
3678
|
+
collection: plan.collection,
|
|
3679
|
+
relPath: plan.relPath,
|
|
3680
|
+
},
|
|
3681
|
+
contentWritten: plan.newHash !== null,
|
|
3682
|
+
};
|
|
3683
|
+
};
|
|
3479
3684
|
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
);
|
|
3494
|
-
// Notify scheduler after sync completes
|
|
3495
|
-
ctxHolder.scheduler?.notifySyncComplete([doc.docid]);
|
|
3496
|
-
ctxHolder.eventBus?.emit({
|
|
3497
|
-
type: "document-changed",
|
|
3498
|
-
uri: doc.uri,
|
|
3499
|
-
collection: doc.collection,
|
|
3500
|
-
relPath: doc.relPath,
|
|
3501
|
-
origin: "save",
|
|
3502
|
-
changedAt: new Date().toISOString(),
|
|
3503
|
-
});
|
|
3504
|
-
return {
|
|
3505
|
-
collections: [result],
|
|
3506
|
-
totalDurationMs: result.durationMs,
|
|
3507
|
-
totalFilesProcessed: result.filesProcessed,
|
|
3508
|
-
totalFilesAdded: result.filesAdded,
|
|
3509
|
-
totalFilesUpdated: result.filesUpdated,
|
|
3510
|
-
totalFilesErrored: result.filesErrored,
|
|
3511
|
-
totalFilesSkipped: result.filesSkipped,
|
|
3512
|
-
};
|
|
3685
|
+
const lockPath =
|
|
3686
|
+
deps?.lockPath ??
|
|
3687
|
+
writeLeasePath(getIndexDbPath(ctxHolder.current?.indexName));
|
|
3688
|
+
const lockWaitMs = deps?.lockWaitMs ?? DEFAULT_LOCK_WAIT_MS;
|
|
3689
|
+
let outcome: DocUpdateOutcome;
|
|
3690
|
+
let request: RequestReceiptInfo | undefined;
|
|
3691
|
+
try {
|
|
3692
|
+
if (requestId === undefined) {
|
|
3693
|
+
outcome = await withWriteLock(
|
|
3694
|
+
lockPath,
|
|
3695
|
+
async () => {
|
|
3696
|
+
const prepared = await prepare();
|
|
3697
|
+
await prepared.publish();
|
|
3698
|
+
return finish(prepared.plan);
|
|
3513
3699
|
},
|
|
3514
|
-
|
|
3700
|
+
lockWaitMs
|
|
3515
3701
|
);
|
|
3516
|
-
|
|
3702
|
+
} else {
|
|
3703
|
+
const requested = await runRequestedWrite<
|
|
3704
|
+
DocUpdatePlan,
|
|
3705
|
+
DocUpdateOutcome
|
|
3706
|
+
>({
|
|
3707
|
+
...localRequestLedger(store.getDbPath()),
|
|
3708
|
+
requestId,
|
|
3709
|
+
operation: "document.update",
|
|
3710
|
+
digest: requestDigest("document.update", {
|
|
3711
|
+
target: body.uri ?? docId,
|
|
3712
|
+
content: body.content,
|
|
3713
|
+
tags: normalizedTags,
|
|
3714
|
+
expectedSourceHash: body.expectedSourceHash,
|
|
3715
|
+
expectedModifiedAt: body.expectedModifiedAt,
|
|
3716
|
+
}),
|
|
3717
|
+
lockPath,
|
|
3718
|
+
lockWaitMs,
|
|
3719
|
+
checkpoint: deps?.requestCheckpoint,
|
|
3720
|
+
prepare,
|
|
3721
|
+
inspect: async (plan) => {
|
|
3722
|
+
if (plan.newHash === null) {
|
|
3723
|
+
// Tags only: resume only if neither the file nor the tags moved
|
|
3724
|
+
// on since admission (or the tags already are this request's).
|
|
3725
|
+
const current = await store.getDocumentByUri(plan.gnoUri);
|
|
3726
|
+
const file = Bun.file(plan.fullPath);
|
|
3727
|
+
if (
|
|
3728
|
+
!current.ok ||
|
|
3729
|
+
!current.value ||
|
|
3730
|
+
!(await file.exists()) ||
|
|
3731
|
+
hashContent(await file.text()) !== plan.baseHash
|
|
3732
|
+
) {
|
|
3733
|
+
return "unexpected";
|
|
3734
|
+
}
|
|
3735
|
+
const tags = await tagState(current.value.id);
|
|
3736
|
+
return tags === plan.baseTagState || tags === plan.resultTagState
|
|
3737
|
+
? "published"
|
|
3738
|
+
: "unexpected";
|
|
3739
|
+
}
|
|
3740
|
+
const file = Bun.file(plan.fullPath);
|
|
3741
|
+
if (!(await file.exists())) return "unexpected";
|
|
3742
|
+
const onDisk = hashContent(await file.text());
|
|
3743
|
+
if (onDisk !== plan.newHash) {
|
|
3744
|
+
return onDisk === plan.baseHash ? "absent" : "unexpected";
|
|
3745
|
+
}
|
|
3746
|
+
if (plan.resultTagState === null) return "published";
|
|
3747
|
+
// Content plus tags: finish rewrites the user tags, so resume only
|
|
3748
|
+
// if no store-only tag write landed since admission. A re-sync of
|
|
3749
|
+
// this request's own file may move frontmatter tags; that is fine.
|
|
3750
|
+
const current = await store.getDocumentByUri(plan.gnoUri);
|
|
3751
|
+
if (!current.ok || !current.value) return "unexpected";
|
|
3752
|
+
const tags = await tagState(current.value.id);
|
|
3753
|
+
return tags === plan.baseTagState ||
|
|
3754
|
+
tags === plan.resultTagState ||
|
|
3755
|
+
(await userTagState(current.value.id)) === plan.baseUserTagState
|
|
3756
|
+
? "published"
|
|
3757
|
+
: "unexpected";
|
|
3758
|
+
},
|
|
3759
|
+
finish,
|
|
3760
|
+
resultRef: ({ body, target }) => ({
|
|
3761
|
+
uri: target.gnoUri,
|
|
3762
|
+
docid: body.docId,
|
|
3763
|
+
sourceHash: body.version.sourceHash,
|
|
3764
|
+
}),
|
|
3765
|
+
});
|
|
3766
|
+
outcome = requested.result;
|
|
3767
|
+
request = requested.request;
|
|
3768
|
+
if (request.replayed) return jsonResponse({ ...outcome.body, request });
|
|
3769
|
+
}
|
|
3770
|
+
} catch (error) {
|
|
3771
|
+
if (error instanceof DocUpdateRejected) return error.response;
|
|
3772
|
+
const receiptError = requestErrorResponse(error);
|
|
3773
|
+
if (receiptError) return receiptError;
|
|
3774
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3775
|
+
if (isLeaseBusy(error)) {
|
|
3776
|
+
return errorResponse(MCP_ERRORS.LOCKED.code, message, 409);
|
|
3517
3777
|
}
|
|
3518
|
-
|
|
3519
|
-
return jsonResponse({
|
|
3520
|
-
success: true,
|
|
3521
|
-
docId: doc.docid,
|
|
3522
|
-
uri: fileUri,
|
|
3523
|
-
path: fullPath,
|
|
3524
|
-
jobId,
|
|
3525
|
-
writeBack,
|
|
3526
|
-
version: {
|
|
3527
|
-
sourceHash: currentSourceHash,
|
|
3528
|
-
modifiedAt: currentModifiedAt,
|
|
3529
|
-
},
|
|
3530
|
-
});
|
|
3531
|
-
} catch (e) {
|
|
3532
3778
|
return errorResponse(
|
|
3533
3779
|
"RUNTIME",
|
|
3534
|
-
`Failed to update document: ${
|
|
3780
|
+
`Failed to update document: ${message}`,
|
|
3535
3781
|
500
|
|
3536
3782
|
);
|
|
3537
3783
|
}
|
|
3784
|
+
|
|
3785
|
+
// Deferred work: lexical sync via the job system, embedding via the
|
|
3786
|
+
// scheduler. Its failure never undoes the committed save.
|
|
3787
|
+
let jobId: string | null = null;
|
|
3788
|
+
const { target } = outcome;
|
|
3789
|
+
const collection = ctxHolder.config.collections.find(
|
|
3790
|
+
(candidate) => candidate.name === target.collection
|
|
3791
|
+
);
|
|
3792
|
+
if (outcome.contentWritten && collection) {
|
|
3793
|
+
const jobResult = await startJob(
|
|
3794
|
+
"sync",
|
|
3795
|
+
async (): Promise<SyncResult> => {
|
|
3796
|
+
const result = await syncResidentCollection(
|
|
3797
|
+
ctxHolder,
|
|
3798
|
+
collection,
|
|
3799
|
+
store,
|
|
3800
|
+
withContentTypeRules({ runUpdateCmd: false }, ctxHolder.config),
|
|
3801
|
+
deps?.syncCollection
|
|
3802
|
+
);
|
|
3803
|
+
// Notify scheduler after sync completes
|
|
3804
|
+
ctxHolder.scheduler?.notifySyncComplete([outcome.body.docId]);
|
|
3805
|
+
ctxHolder.eventBus?.emit({
|
|
3806
|
+
type: "document-changed",
|
|
3807
|
+
uri: target.gnoUri,
|
|
3808
|
+
collection: target.collection,
|
|
3809
|
+
relPath: target.relPath,
|
|
3810
|
+
origin: "save",
|
|
3811
|
+
changedAt: new Date().toISOString(),
|
|
3812
|
+
});
|
|
3813
|
+
return {
|
|
3814
|
+
collections: [result],
|
|
3815
|
+
totalDurationMs: result.durationMs,
|
|
3816
|
+
totalFilesProcessed: result.filesProcessed,
|
|
3817
|
+
totalFilesAdded: result.filesAdded,
|
|
3818
|
+
totalFilesUpdated: result.filesUpdated,
|
|
3819
|
+
totalFilesErrored: result.filesErrored,
|
|
3820
|
+
totalFilesSkipped: result.filesSkipped,
|
|
3821
|
+
};
|
|
3822
|
+
},
|
|
3823
|
+
ctxHolder.jobManager
|
|
3824
|
+
);
|
|
3825
|
+
jobId = jobResult.ok ? jobResult.jobId : null;
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
return jsonResponse({
|
|
3829
|
+
...outcome.body,
|
|
3830
|
+
jobId,
|
|
3831
|
+
...(request ? { request } : {}),
|
|
3832
|
+
});
|
|
3538
3833
|
}
|
|
3539
3834
|
|
|
3540
3835
|
/**
|
|
@@ -3698,8 +3993,39 @@ export async function handleCreateCapture(
|
|
|
3698
3993
|
);
|
|
3699
3994
|
}
|
|
3700
3995
|
|
|
3996
|
+
const { requestId, ...captureInput } = body;
|
|
3997
|
+
if (requestId !== undefined) {
|
|
3998
|
+
try {
|
|
3999
|
+
const result = await executeRequestedResidentCapture(
|
|
4000
|
+
ctxHolder,
|
|
4001
|
+
store,
|
|
4002
|
+
{ ...captureInput, collection: body.collection },
|
|
4003
|
+
validateRequestId(requestId),
|
|
4004
|
+
deps
|
|
4005
|
+
);
|
|
4006
|
+
return jsonResponse(result.body, result.status);
|
|
4007
|
+
} catch (error) {
|
|
4008
|
+
if (error instanceof ResidentCapturePlanError) {
|
|
4009
|
+
return errorResponse(
|
|
4010
|
+
error.shape.code,
|
|
4011
|
+
error.shape.message,
|
|
4012
|
+
error.shape.status
|
|
4013
|
+
);
|
|
4014
|
+
}
|
|
4015
|
+
const receiptError = requestErrorResponse(error);
|
|
4016
|
+
if (receiptError) return receiptError;
|
|
4017
|
+
const shape = classifyResidentCaptureError(error);
|
|
4018
|
+
return errorResponse(
|
|
4019
|
+
shape.code,
|
|
4020
|
+
shape.message,
|
|
4021
|
+
shape.status,
|
|
4022
|
+
shape.details
|
|
4023
|
+
);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
|
|
3701
4027
|
const planned = await planResidentCapture(ctxHolder, store, {
|
|
3702
|
-
...
|
|
4028
|
+
...captureInput,
|
|
3703
4029
|
collection: body.collection,
|
|
3704
4030
|
});
|
|
3705
4031
|
if (!planned.ok) {
|
|
@@ -3765,6 +4091,8 @@ export interface MemoryRouteDeps {
|
|
|
3765
4091
|
}
|
|
3766
4092
|
|
|
3767
4093
|
function memoryErrorResponse(error: unknown, fallback: string): Response {
|
|
4094
|
+
const receiptError = requestErrorResponse(error);
|
|
4095
|
+
if (receiptError) return receiptError;
|
|
3768
4096
|
if (error instanceof MemoryError) {
|
|
3769
4097
|
return errorResponse(
|
|
3770
4098
|
error.code,
|
|
@@ -3824,6 +4152,7 @@ function createMemoryService(
|
|
|
3824
4152
|
collections: ctx.config.collections,
|
|
3825
4153
|
lockPath: deps.lockPath ?? writeLeasePath(getIndexDbPath(ctx.indexName)),
|
|
3826
4154
|
lockWaitMs: deps.lockWaitMs,
|
|
4155
|
+
requests: localRequestLedger(store.getDbPath()),
|
|
3827
4156
|
embedPort: ctx.embedPort,
|
|
3828
4157
|
vectorIndex: ctx.vectorIndex,
|
|
3829
4158
|
});
|