@loicngr/kobo 1.11.39 → 1.11.40
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/AGENTS.md +12 -0
- package/CHANGELOG.md +4 -0
- package/dist/mcp-server/kobo-tasks-handlers.js +16 -9
- package/dist/mcp-server/kobo-tasks-server.js +13 -2
- package/dist/mcp-server/workspace-dialogue-client.js +28 -0
- package/dist/server/db/mcp-message-schema.js +16 -0
- package/dist/server/db/migrations.js +4 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/db/search-schema.js +53 -0
- package/dist/server/index.js +17 -86
- package/dist/server/middleware/network-auth-middleware.js +5 -1
- package/dist/server/routes/changelog.js +4 -49
- package/dist/server/routes/git.js +2 -2
- package/dist/server/routes/mcp.js +47 -0
- package/dist/server/routes/pull-requests.js +1 -1
- package/dist/server/routes/search.js +13 -4
- package/dist/server/routes/settings.js +5 -0
- package/dist/server/routes/workspaces.js +225 -241
- package/dist/server/services/agent/engines/claude-code/engine.js +31 -33
- package/dist/server/services/agent/engines/codex/engine.js +37 -8
- package/dist/server/services/agent/engines/codex/event-mapper.js +3 -1
- package/dist/server/services/agent/engines/codex/options-builder.js +4 -3
- package/dist/server/services/agent/engines/codex/server-requests.js +8 -1
- package/dist/server/services/agent/orchestrator.js +20 -3
- package/dist/server/services/change-source-branch-service.js +10 -42
- package/dist/server/services/chat-receipt-service.js +33 -0
- package/dist/server/services/cron-service.js +27 -1
- package/dist/server/services/mcp-client-context.js +12 -0
- package/dist/server/services/mcp-connection-info-service.js +63 -0
- package/dist/server/services/mcp-message-delivery-service.js +59 -0
- package/dist/server/services/mcp-message-request-service.js +64 -0
- package/dist/server/services/pr-checkout-service.js +10 -10
- package/dist/server/services/pr-watcher-service.js +14 -9
- package/dist/server/services/search/indexer.js +232 -0
- package/dist/server/services/search/worker.js +76 -0
- package/dist/server/services/search-service.js +104 -110
- package/dist/server/services/update-check-service.js +119 -0
- package/dist/server/services/websocket-service.js +121 -35
- package/dist/server/services/workspace-dialogue-service.js +128 -0
- package/dist/server/services/workspace-message-service.js +58 -0
- package/dist/server/services/workspace-permission-policy-service.js +33 -3
- package/dist/server/services/worktree-restore-service.js +2 -2
- package/dist/server/services/worktree-service.js +197 -124
- package/dist/server/utils/bounded-process.js +137 -0
- package/dist/server/utils/git-ops.js +76 -128
- package/dist/server/utils/git-repo-lock.js +27 -13
- package/dist/shared/workspace-dialogue-tools.js +89 -0
- package/dist/shared/workspace-message-types.js +19 -0
- package/package.json +2 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-Chsw1Mh7.css → ActivityFeed-BYR5x7aa.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-MqsC-95i.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-D8563008.js → ChangelogPage-BxURWYxa.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bd0X08sg.js → ComparisonPanel-DjiMDAUi.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-B-rhBmqv.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-mA5RZf1t.js +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-Ci7ucmy3.js +8 -0
- package/src/client/dist/pwa/assets/{DiffViewer-DnR79ycb.css → DiffViewer-DEPOa3D7.css} +1 -1
- package/src/client/dist/pwa/assets/{DocumentsPanel-BJsJ0fNs.js → DocumentsPanel-RZTvcJkT.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BVLO5K5d.js → DrawerToggleButton-CTOc3yA0.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-_kL2gMoy.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-RFZS7Lao.js → HealthPage-D3__hKJ5.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-B3JKUm3n.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-X8oNU5Mb.js → QBtnDropdown-td0RAVPx.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-Cij17skX.js → QDrawer-BkyFPV7a.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-WspK3KC7.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-CwcCGSWq.js → QTooltip-7pw7iuV0.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-CLNM13oo.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGc1oQSM.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-CmRgIvu4.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-Cj6Y_TlN.js +9 -0
- package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +1 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D7lGFRKs.js → SplitWorkspacePage-CCHGEsKf.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DI8DHb9e.js → TerminalPanel-ijmsYk-2.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-B3nrkCY8.js → TouchPan-DuQFjlQF.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DU1UShMY.js → TourReplayButton-C5RF0lOK.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-C-yN5IX8.js → WorkspaceAttentionLabels-BR_XElog.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-BGCildyP.js +4 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-BAC6u9OP.css → WorkspacePage-CqcMweNL.css} +1 -1
- package/src/client/dist/pwa/assets/{browser-f9MHQjfi.js → browser-DUqzdkbg.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BWBzP8os.js → build-path-tree-ImiSpzfI.js} +1 -1
- package/src/client/dist/pwa/assets/de-CaHID9mW.js +17 -0
- package/src/client/dist/pwa/assets/{documents-C2cLnEBQ.js → documents-2C1hn1pw.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-sw146MWN.js → editor.api-Bm1vVaBr.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-C5ZZgCH4.js → efforts-_D8DFYXo.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-CZ9qfBYq.js +1 -0
- package/src/client/dist/pwa/assets/es-ZhzlCmer.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-B6xAlAaM.js → expand-template-CbDNnTPP.js} +1 -1
- package/src/client/dist/pwa/assets/{format-BwWP6Aay.js → format-pPImdFUy.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DyWtO-nR.js → formatters-dTX38ZsV.js} +1 -1
- package/src/client/dist/pwa/assets/fr-BfXw7L7J.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DZ5Hm8UH.js → freemarker2-DGluDnAm.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BRUGpTA7.js → handlebars-BgviTnpD.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DNOayNNs.js → html-CV-MrunL.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CmXBenUp.js +1 -0
- package/src/client/dist/pwa/assets/index-9tiw3NjK.js +20 -0
- package/src/client/dist/pwa/assets/it-CHAurcTF.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-C0tDQU3z.js → javascript-C2VFcHf2.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CN46O6OF.js → liquid-Dg9ZhbP5.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DM-lrr8Q.js → mdx-ImgrPYNl.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CkN7y-8c.js → monaco-contributions-waWZXS1T.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-2BcAOLjc.js → monaco.contribution-BB2rtCHM.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-h6O0qCMA.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-a91Tcr6o.js → notify-theme-Dyg-FGI6.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-DOtELZfB.js → pending-diff-open-Dfpg92li.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-C-9Crj-c.js → permissionModes-DDWmNBW1.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BRe4oyNx.js → project-color-XUfY_zbw.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BRpJG6sZ.js → python-DVLs-DBA.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CehCmvHl.js → razor-D34BqMwB.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DlbQLZ02.js → render-chat-markdown-DwUkPy-O.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dt_8mVuj.js → typescript-C7-UpNXx.js} +1 -1
- package/src/client/dist/pwa/assets/use-search-index-status-DtLzygcy.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-DfE-kwrX.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-GCObcjO6.js → workspace-templates-DwwdOEAq.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-_gOwdzB4.js → xml-Cx1oQhEr.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BxeBqpGf.js → yaml-BQOiJsdx.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +118 -2
- package/src/mcp-server/kobo-tasks-handlers.ts +14 -11
- package/src/mcp-server/kobo-tasks-server.ts +19 -2
- package/src/mcp-server/workspace-dialogue-client.ts +34 -0
- package/src/client/dist/pwa/assets/ActivityFeed-BzzvcOmq.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-Bon8qIka.js +0 -2
- package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-BpsLNxnj.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-Cal_wzC0.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-vvyiRcql.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-xd-AlGo_.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-CXRUsz6G.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-CVgRa025.css +0 -1
- package/src/client/dist/pwa/assets/SearchPage-DopbgLxh.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Do9d0Pov.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Jx6TnOzR.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +0 -4
- package/src/client/dist/pwa/assets/de-QkDLrcGq.js +0 -17
- package/src/client/dist/pwa/assets/engineFeatures-CAVMDawq.js +0 -1
- package/src/client/dist/pwa/assets/es-BgQFKlo8.js +0 -17
- package/src/client/dist/pwa/assets/fr-DAdLWjNM.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CBez_vip.js +0 -1
- package/src/client/dist/pwa/assets/index-VL3fI9a3.js +0 -20
- package/src/client/dist/pwa/assets/it-Bumaewp1.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-KYS4snQH.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-CSVM_54K.js +0 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getDb } from '../db/index.js';
|
|
2
|
+
import { finishMessageRequest, markMessageDispatching, reserveMessageRequest, } from './mcp-message-request-service.js';
|
|
3
|
+
import { broadcastPersistedEvent, persistWorkspaceEvent } from './websocket-service.js';
|
|
4
|
+
import { deliverWorkspaceMessage } from './workspace-message-service.js';
|
|
5
|
+
export async function deliverKeyedMcpMessage(workspaceId, key, message) {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
const request = reserveMessageRequest(db, workspaceId, key, message.content, message.sessionId);
|
|
8
|
+
if (!request.fresh)
|
|
9
|
+
return request.result;
|
|
10
|
+
let dispatched = false;
|
|
11
|
+
let accepted;
|
|
12
|
+
try {
|
|
13
|
+
await deliverWorkspaceMessage(workspaceId, { ...message, clientMessageId: request.requestId }, {
|
|
14
|
+
beforeDispatch() {
|
|
15
|
+
// The normal closed-engine fallback may enter this boundary again
|
|
16
|
+
// while resuming the very same request, never a second reservation.
|
|
17
|
+
if (dispatched)
|
|
18
|
+
return;
|
|
19
|
+
markMessageDispatching(db, request.requestId);
|
|
20
|
+
dispatched = true;
|
|
21
|
+
},
|
|
22
|
+
persist(payload, sessionId) {
|
|
23
|
+
const committed = db.transaction(() => {
|
|
24
|
+
const event = persistWorkspaceEvent(workspaceId, 'user:message', payload, sessionId);
|
|
25
|
+
const result = { accepted: true, requestId: request.requestId, eventId: event.id, sessionId };
|
|
26
|
+
finishMessageRequest(db, request.requestId, 'accepted', result);
|
|
27
|
+
return { event, result };
|
|
28
|
+
})();
|
|
29
|
+
accepted = committed.result;
|
|
30
|
+
broadcastPersistedEvent(committed.event);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
if (!accepted)
|
|
34
|
+
throw new Error('Delivery completed without a durable receipt');
|
|
35
|
+
return accepted;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// A notification failure cannot invalidate an already committed receipt.
|
|
39
|
+
if (accepted)
|
|
40
|
+
return accepted;
|
|
41
|
+
const result = {
|
|
42
|
+
accepted: false,
|
|
43
|
+
requestId: request.requestId,
|
|
44
|
+
code: dispatched ? 'delivery_unknown' : 'delivery_rejected',
|
|
45
|
+
message: dispatched
|
|
46
|
+
? 'Delivery may have occurred; inspect history before sending a new request'
|
|
47
|
+
: error instanceof Error
|
|
48
|
+
? error.message
|
|
49
|
+
: String(error),
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
finishMessageRequest(db, request.requestId, dispatched ? 'unknown' : 'rejected', result);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* The reserved/dispatching row still prevents redelivery if storage failed. */
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
export function reserveMessageRequest(db, workspaceId, key, content, sessionId) {
|
|
4
|
+
const fingerprint = createHash('sha256')
|
|
5
|
+
.update(JSON.stringify([content, sessionId ?? null]))
|
|
6
|
+
.digest('hex');
|
|
7
|
+
return db
|
|
8
|
+
.transaction(() => {
|
|
9
|
+
const requestId = `mcp-${nanoid()}`;
|
|
10
|
+
const now = new Date().toISOString();
|
|
11
|
+
const inserted = db
|
|
12
|
+
.prepare(`INSERT INTO mcp_message_requests(workspace_id,idempotency_key,request_id,fingerprint,state,created_at,updated_at)
|
|
13
|
+
VALUES (?,?,?,?,'reserved',?,?) ON CONFLICT(workspace_id,idempotency_key) DO NOTHING`)
|
|
14
|
+
.run(workspaceId, key, requestId, fingerprint, now, now);
|
|
15
|
+
if (inserted.changes)
|
|
16
|
+
return { fresh: true, requestId };
|
|
17
|
+
const row = db
|
|
18
|
+
.prepare('SELECT request_id,fingerprint,state,result_json FROM mcp_message_requests WHERE workspace_id=? AND idempotency_key=?')
|
|
19
|
+
.get(workspaceId, key);
|
|
20
|
+
let result;
|
|
21
|
+
if (row.fingerprint !== fingerprint)
|
|
22
|
+
result = {
|
|
23
|
+
accepted: false,
|
|
24
|
+
requestId: row.request_id,
|
|
25
|
+
code: 'idempotency_conflict',
|
|
26
|
+
message: 'This key was already used for different content or a different session',
|
|
27
|
+
};
|
|
28
|
+
else if (row.result_json) {
|
|
29
|
+
result = JSON.parse(row.result_json);
|
|
30
|
+
if (result.accepted)
|
|
31
|
+
result = { ...result, replayed: true };
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
result = {
|
|
35
|
+
accepted: false,
|
|
36
|
+
requestId: row.request_id,
|
|
37
|
+
code: row.state === 'unknown' ? 'delivery_unknown' : 'in_progress',
|
|
38
|
+
message: row.state === 'unknown'
|
|
39
|
+
? 'Delivery may have occurred; inspect history before sending a new request'
|
|
40
|
+
: 'This request is still in progress; retry the same key to check its outcome',
|
|
41
|
+
};
|
|
42
|
+
return { fresh: false, requestId: row.request_id, result };
|
|
43
|
+
})
|
|
44
|
+
.immediate();
|
|
45
|
+
}
|
|
46
|
+
export function markMessageDispatching(db, requestId) {
|
|
47
|
+
const result = db
|
|
48
|
+
.prepare("UPDATE mcp_message_requests SET state='dispatching',updated_at=? WHERE request_id=? AND state='reserved'")
|
|
49
|
+
.run(new Date().toISOString(), requestId);
|
|
50
|
+
if (result.changes !== 1)
|
|
51
|
+
throw new Error('MCP request is no longer reserved');
|
|
52
|
+
}
|
|
53
|
+
export function finishMessageRequest(db, requestId, state, result) {
|
|
54
|
+
const changed = db
|
|
55
|
+
.prepare(`UPDATE mcp_message_requests SET state=?,result_json=?,event_id=?,session_id=?,updated_at=?
|
|
56
|
+
WHERE request_id=? AND state IN ('reserved','dispatching')`)
|
|
57
|
+
.run(state, JSON.stringify(result), result.accepted ? result.eventId : null, result.accepted ? (result.sessionId ?? null) : null, new Date().toISOString(), requestId);
|
|
58
|
+
if (changed.changes !== 1)
|
|
59
|
+
throw new Error('MCP request is no longer pending');
|
|
60
|
+
}
|
|
61
|
+
/** Backend startup only: never assume an interrupted engine delivery did not happen. */
|
|
62
|
+
export function reconcileMessageRequests(db) {
|
|
63
|
+
db.prepare("UPDATE mcp_message_requests SET state='unknown',updated_at=? WHERE state IN ('reserved','dispatching')").run(new Date().toISOString());
|
|
64
|
+
}
|
|
@@ -9,7 +9,7 @@ import { fingerprintWorktree } from '../utils/git-worktree-fingerprint.js';
|
|
|
9
9
|
import { withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
|
|
10
10
|
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
11
11
|
import { hasController } from './agent/orchestrator.js';
|
|
12
|
-
import {
|
|
12
|
+
import { createWorktreeUnlocked, listWorktrees, removeWorktreeUnlocked } from './worktree-service.js';
|
|
13
13
|
/**
|
|
14
14
|
* A `pathCollision.worktreePath` decision is untrusted input reaching
|
|
15
15
|
* `git worktree add` directly — including from a network-access client that
|
|
@@ -81,9 +81,9 @@ function hasCommonAncestor(repoPath, branch) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
/** Locate any worktree already sitting on `branch`, or at the target path. */
|
|
84
|
-
function diagnoseWorktree(projectPath, branch, targetPath, attachedPaths) {
|
|
84
|
+
async function diagnoseWorktree(projectPath, branch, targetPath, attachedPaths) {
|
|
85
85
|
const blockers = [];
|
|
86
|
-
const worktrees = listWorktrees(projectPath);
|
|
86
|
+
const worktrees = await listWorktrees(projectPath);
|
|
87
87
|
const onBranch = worktrees.find((wt) => wt.branch === branch);
|
|
88
88
|
if (onBranch) {
|
|
89
89
|
if (!fs.existsSync(onBranch.path)) {
|
|
@@ -163,7 +163,7 @@ export async function computeFingerprint(report) {
|
|
|
163
163
|
* `attachedPaths` maps worktree path -> workspace id; the route supplies it so
|
|
164
164
|
* this function stays free of database access and easy to test.
|
|
165
165
|
*/
|
|
166
|
-
export function diagnoseLocalState(projectPath, headBranch, worktreesPath, attachedPaths = new Map(), workspaces = []) {
|
|
166
|
+
export async function diagnoseLocalState(projectPath, headBranch, worktreesPath, attachedPaths = new Map(), workspaces = []) {
|
|
167
167
|
const targetWorktreePath = resolveWorkspaceWorktreePath(projectPath, headBranch, worktreesPath);
|
|
168
168
|
const blockers = [];
|
|
169
169
|
const lockPath = gitOps.getIndexLockPath(projectPath);
|
|
@@ -174,7 +174,7 @@ export function diagnoseLocalState(projectPath, headBranch, worktreesPath, attac
|
|
|
174
174
|
!hasCommonAncestor(projectPath, headBranch)) {
|
|
175
175
|
blockers.push({ kind: 'no-common-ancestor', branch: headBranch });
|
|
176
176
|
}
|
|
177
|
-
const { worktree, blockers: worktreeBlockers } = diagnoseWorktree(projectPath, headBranch, targetWorktreePath, attachedPaths);
|
|
177
|
+
const { worktree, blockers: worktreeBlockers } = await diagnoseWorktree(projectPath, headBranch, targetWorktreePath, attachedPaths);
|
|
178
178
|
blockers.push(...worktreeBlockers);
|
|
179
179
|
const inspectPath = worktree.state === 'orphan' || worktree.state === 'attached' ? worktree.path : null;
|
|
180
180
|
return {
|
|
@@ -256,8 +256,8 @@ export class StaleDiagnosisError extends Error {
|
|
|
256
256
|
* `change-source-branch-service`, so it cannot interleave with a worktree purge
|
|
257
257
|
* or a source-branch change.
|
|
258
258
|
*/
|
|
259
|
-
export function resolvePrCheckout(input) {
|
|
260
|
-
const initial = diagnoseLocalState(input.projectPath, input.headBranch, input.worktreesPath, input.attachedPaths, input.workspaces);
|
|
259
|
+
export async function resolvePrCheckout(input) {
|
|
260
|
+
const initial = await diagnoseLocalState(input.projectPath, input.headBranch, input.worktreesPath, input.attachedPaths, input.workspaces);
|
|
261
261
|
const owner = initial.worktree.state === 'attached'
|
|
262
262
|
? initial.worktree.workspaceId
|
|
263
263
|
: initial.workspace.state !== 'none'
|
|
@@ -274,7 +274,7 @@ function resolvePrCheckoutLocked(input, owner) {
|
|
|
274
274
|
// 1. Refresh, then refuse a plan built on facts that no longer hold.
|
|
275
275
|
gitOps.fetchSourceBranch(input.projectPath, input.headBranch);
|
|
276
276
|
applied.push({ kind: 'fetch', detail: `origin/${input.headBranch}` });
|
|
277
|
-
const fresh = diagnoseLocalState(input.projectPath, input.headBranch, input.worktreesPath, input.attachedPaths ?? new Map(), input.workspaces ?? []);
|
|
277
|
+
const fresh = await diagnoseLocalState(input.projectPath, input.headBranch, input.worktreesPath, input.attachedPaths ?? new Map(), input.workspaces ?? []);
|
|
278
278
|
if ((await computeFingerprint(fresh)) !== input.fingerprint)
|
|
279
279
|
throw new StaleDiagnosisError(fresh);
|
|
280
280
|
if ((fresh.worktree.state === 'orphan' || fresh.worktree.state === 'attached') &&
|
|
@@ -346,7 +346,7 @@ function resolvePrCheckoutLocked(input, owner) {
|
|
|
346
346
|
const baseRef = gitOps.localBranchExists(input.projectPath, input.headBranch)
|
|
347
347
|
? input.headBranch
|
|
348
348
|
: `origin/${input.headBranch}`;
|
|
349
|
-
const target =
|
|
349
|
+
const target = await createWorktreeUnlocked(input.projectPath, input.headBranch, baseRef, input.worktreesPath, undefined, explicitPath);
|
|
350
350
|
worktreePath = target.worktreePath;
|
|
351
351
|
created = true;
|
|
352
352
|
applied.push({ kind: 'create-worktree', detail: worktreePath });
|
|
@@ -361,7 +361,7 @@ function resolvePrCheckoutLocked(input, owner) {
|
|
|
361
361
|
catch (err) {
|
|
362
362
|
if (created) {
|
|
363
363
|
try {
|
|
364
|
-
removeWorktreeUnlocked(input.projectPath, worktreePath);
|
|
364
|
+
await removeWorktreeUnlocked(input.projectPath, worktreePath);
|
|
365
365
|
}
|
|
366
366
|
catch (cleanupErr) {
|
|
367
367
|
console.error('[pr-checkout] rollback failed to remove the worktree:', cleanupErr);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { fetchSourceBranchAsync } from '../utils/git-ops.js';
|
|
3
3
|
import { withGitRepoLock } from '../utils/git-repo-lock.js';
|
|
4
|
-
import { isWorkspaceLifecycleBusy } from '../utils/workspace-lifecycle-guard.js';
|
|
4
|
+
import { isWorkspaceLifecycleBusy, withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
|
|
5
5
|
import { hasController } from './agent/orchestrator.js';
|
|
6
6
|
import { stopDevServer } from './dev-server-service.js';
|
|
7
7
|
import { getForgeProvider } from './forge/registry.js';
|
|
@@ -99,19 +99,24 @@ function markUnread(workspaceId) {
|
|
|
99
99
|
console.error('[pr-watcher] markUnread failed:', err instanceof Error ? err.message : err);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
function autoRestoreManuallyRecreatedWorktrees() {
|
|
102
|
+
async function autoRestoreManuallyRecreatedWorktrees() {
|
|
103
103
|
for (const ws of listArchivedWorkspaces()) {
|
|
104
104
|
if (!ws.worktreePurgedAt || !ws.worktreeOwned || isWorkspaceLifecycleBusy(ws.id))
|
|
105
105
|
continue;
|
|
106
106
|
if (!fs.existsSync(ws.worktreePath))
|
|
107
107
|
continue;
|
|
108
|
-
if (!isMatchingWorkspaceWorktree(ws))
|
|
109
|
-
continue;
|
|
110
108
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
await withWorkspaceLifecycleGuard(ws.id, async () => {
|
|
110
|
+
const current = getWorkspace(ws.id);
|
|
111
|
+
if (!current?.worktreePurgedAt || !current.worktreeOwned)
|
|
112
|
+
return;
|
|
113
|
+
if (!(await isMatchingWorkspaceWorktree(current)))
|
|
114
|
+
return;
|
|
115
|
+
const restored = restoreWorktreeFromDisk(ws.id);
|
|
116
|
+
invalidateWorkspacePrCaches(ws.id);
|
|
117
|
+
emitEphemeral(ws.id, 'workspace:worktree-restored', { workspace: restored });
|
|
118
|
+
console.log(`[pr-watcher] auto-restored worktree for workspace '${ws.name}' (manual restore detected)`);
|
|
119
|
+
});
|
|
115
120
|
}
|
|
116
121
|
catch (err) {
|
|
117
122
|
console.error(`[pr-watcher] auto-restore failed for '${ws.name}':`, err instanceof Error ? err.message : err);
|
|
@@ -119,7 +124,7 @@ function autoRestoreManuallyRecreatedWorktrees() {
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
export async function checkPrStatuses() {
|
|
122
|
-
autoRestoreManuallyRecreatedWorktrees();
|
|
127
|
+
await autoRestoreManuallyRecreatedWorktrees();
|
|
123
128
|
const workspaces = listWorkspaces(false); // non-archived only
|
|
124
129
|
// Clean up entries for workspaces that no longer exist
|
|
125
130
|
for (const id of lastKnownPr.keys()) {
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
export const normalizeSearchText = (text) => text.normalize('NFC').toLowerCase();
|
|
2
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
3
|
+
function readable(row) {
|
|
4
|
+
let p;
|
|
5
|
+
try {
|
|
6
|
+
p = JSON.parse(row.payload);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
if (!p || typeof p !== 'object')
|
|
12
|
+
return null;
|
|
13
|
+
let text;
|
|
14
|
+
let messageId = row.id;
|
|
15
|
+
if (row.type === 'user:message' && typeof p.content === 'string')
|
|
16
|
+
text = p.content;
|
|
17
|
+
if (row.type === 'agent:event' && p.kind === 'message:text' && typeof p.text === 'string') {
|
|
18
|
+
text = p.text;
|
|
19
|
+
if (typeof p.messageId === 'string')
|
|
20
|
+
messageId = p.messageId;
|
|
21
|
+
}
|
|
22
|
+
if (row.type === 'agent:output') {
|
|
23
|
+
const content = p.message?.content;
|
|
24
|
+
if (Array.isArray(content))
|
|
25
|
+
text = content.flatMap((b) => (b?.type === 'text' && typeof b.text === 'string' ? [b.text] : [])).join('\n');
|
|
26
|
+
}
|
|
27
|
+
return text === undefined
|
|
28
|
+
? null
|
|
29
|
+
: { text, key: JSON.stringify([row.workspace_id, row.session_id, row.type, messageId]) };
|
|
30
|
+
}
|
|
31
|
+
/** Map lowercased UTF-16 offsets back to display text (e.g. İ expands to i + dot). */
|
|
32
|
+
function snippet(text, match, needleLength) {
|
|
33
|
+
let normalizedOffset = 0;
|
|
34
|
+
let displayOffset = 0;
|
|
35
|
+
let startOffset = 0;
|
|
36
|
+
let endOffset = text.length;
|
|
37
|
+
for (const character of text) {
|
|
38
|
+
if (normalizedOffset <= match)
|
|
39
|
+
startOffset = displayOffset;
|
|
40
|
+
normalizedOffset += character.toLowerCase().length;
|
|
41
|
+
displayOffset += character.length;
|
|
42
|
+
if (normalizedOffset >= match + needleLength) {
|
|
43
|
+
endOffset = displayOffset;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const start = Math.max(0, startOffset - 100);
|
|
48
|
+
const end = Math.min(text.length, endOffset + 100);
|
|
49
|
+
return `${start ? '…' : ''}${text.slice(start, end).trim()}${end < text.length ? '…' : ''}`;
|
|
50
|
+
}
|
|
51
|
+
/** Runs on a worker connection. Every batch is atomic and restartable. */
|
|
52
|
+
export class SearchIndexer {
|
|
53
|
+
db;
|
|
54
|
+
constructor(db) {
|
|
55
|
+
this.db = db;
|
|
56
|
+
}
|
|
57
|
+
state() {
|
|
58
|
+
return this.db.prepare('SELECT * FROM search_index_state WHERE id = 1').get();
|
|
59
|
+
}
|
|
60
|
+
status() {
|
|
61
|
+
const state = this.state();
|
|
62
|
+
const pending = this.db.prepare('SELECT 1 FROM search_changes LIMIT 1').get();
|
|
63
|
+
return {
|
|
64
|
+
state: state.complete && !pending ? 'ready' : 'building',
|
|
65
|
+
processed: state.processed,
|
|
66
|
+
total: Math.max(state.processed, state.total, 0),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
rebuild(keys) {
|
|
70
|
+
const fragments = this.db.prepare('SELECT f.* FROM search_fragments f JOIN ws_events e ON e.id = f.event_id WHERE f.message_key = ? ORDER BY e.rowid');
|
|
71
|
+
const write = this.db.prepare(`INSERT INTO search_messages(message_key, workspace_id, session_id, event_id, type, created_at, text, normalized)
|
|
72
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(message_key) DO UPDATE SET
|
|
73
|
+
workspace_id=excluded.workspace_id, session_id=excluded.session_id, event_id=excluded.event_id,
|
|
74
|
+
type=excluded.type, created_at=excluded.created_at, text=excluded.text, normalized=excluded.normalized`);
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
const rows = fragments.all(key);
|
|
77
|
+
const first = rows[0];
|
|
78
|
+
if (!first) {
|
|
79
|
+
this.db.prepare('DELETE FROM search_messages WHERE message_key = ?').run(key);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const text = rows
|
|
83
|
+
.map((row) => row.text)
|
|
84
|
+
.join('')
|
|
85
|
+
.normalize('NFC');
|
|
86
|
+
write.run(key, first.workspace_id, first.session_id, first.event_id, first.type, first.created_at, text, normalizeSearchText(text));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
consume(row, eventId, order, keys) {
|
|
90
|
+
const old = this.db
|
|
91
|
+
.prepare('SELECT message_key, event_order FROM search_fragments WHERE event_id = ?')
|
|
92
|
+
.get(eventId);
|
|
93
|
+
if (old)
|
|
94
|
+
keys.add(old.message_key);
|
|
95
|
+
const value = row ? readable(row) : null;
|
|
96
|
+
if (!value || !row) {
|
|
97
|
+
this.db.prepare('DELETE FROM search_fragments WHERE event_id = ?').run(eventId);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
keys.add(value.key);
|
|
101
|
+
this.db
|
|
102
|
+
.prepare(`INSERT INTO search_fragments(event_id, message_key, workspace_id, session_id, type, created_at, event_order, text)
|
|
103
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(event_id) DO UPDATE SET
|
|
104
|
+
message_key=excluded.message_key, workspace_id=excluded.workspace_id, session_id=excluded.session_id,
|
|
105
|
+
type=excluded.type, created_at=excluded.created_at, text=excluded.text`)
|
|
106
|
+
.run(row.id, value.key, row.workspace_id, row.session_id, row.type, row.created_at, old?.event_order ?? order, value.text);
|
|
107
|
+
}
|
|
108
|
+
tick(batchSize = 500) {
|
|
109
|
+
// Acquire the writer reservation before reading: a deferred WAL snapshot
|
|
110
|
+
// cannot be upgraded after another connection commits (SQLITE_BUSY_SNAPSHOT).
|
|
111
|
+
this.db
|
|
112
|
+
.transaction(() => {
|
|
113
|
+
const state = this.state();
|
|
114
|
+
if (state.total < 0)
|
|
115
|
+
this.db.prepare('UPDATE search_index_state SET total = (SELECT count(*) FROM ws_events) WHERE id = 1').run();
|
|
116
|
+
const keys = new Set();
|
|
117
|
+
const changeBudget = state.complete ? batchSize : Math.max(1, Math.floor(batchSize / 2));
|
|
118
|
+
const changes = this.db.prepare('SELECT * FROM search_changes ORDER BY id LIMIT ?').all(changeBudget);
|
|
119
|
+
for (const change of changes) {
|
|
120
|
+
const row = this.db.prepare('SELECT rowid AS rid, * FROM ws_events WHERE id = ?').get(change.event_id);
|
|
121
|
+
this.consume(row, change.event_id, change.operation === 'insert' ? state.base_order + change.id : (row?.rid ?? 0), keys);
|
|
122
|
+
}
|
|
123
|
+
if (changes.length)
|
|
124
|
+
this.db.prepare('DELETE FROM search_changes WHERE id <= ?').run(changes.at(-1).id);
|
|
125
|
+
const backfillBudget = batchSize - changes.length;
|
|
126
|
+
if (!state.complete && backfillBudget > 0) {
|
|
127
|
+
const rows = this.db
|
|
128
|
+
.prepare('SELECT rowid AS rid, * FROM ws_events WHERE id > ? ORDER BY id LIMIT ?')
|
|
129
|
+
.all(state.cursor, backfillBudget);
|
|
130
|
+
for (const row of rows)
|
|
131
|
+
this.consume(row, row.id, row.rid, keys);
|
|
132
|
+
this.db
|
|
133
|
+
.prepare('UPDATE search_index_state SET cursor = ?, processed = processed + ?, complete = ? WHERE id = 1')
|
|
134
|
+
.run(rows.at(-1)?.id ?? state.cursor, rows.length, rows.length < backfillBudget ? 1 : 0);
|
|
135
|
+
}
|
|
136
|
+
this.rebuild(keys);
|
|
137
|
+
})
|
|
138
|
+
.immediate();
|
|
139
|
+
return this.status();
|
|
140
|
+
}
|
|
141
|
+
/** Drain deletions ahead of queries, including those behind an insertion backlog. */
|
|
142
|
+
removeDeleted() {
|
|
143
|
+
this.db
|
|
144
|
+
.transaction(() => {
|
|
145
|
+
const rows = this.db
|
|
146
|
+
.prepare(`SELECT DISTINCT f.event_id, f.message_key FROM search_changes c
|
|
147
|
+
JOIN search_fragments f ON f.event_id = c.event_id
|
|
148
|
+
WHERE NOT EXISTS (SELECT 1 FROM ws_events e WHERE e.id = c.event_id)`)
|
|
149
|
+
.all();
|
|
150
|
+
const keys = new Set();
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
keys.add(row.message_key);
|
|
153
|
+
this.db.prepare('DELETE FROM search_fragments WHERE event_id = ?').run(row.event_id);
|
|
154
|
+
}
|
|
155
|
+
this.rebuild(keys);
|
|
156
|
+
})
|
|
157
|
+
.immediate();
|
|
158
|
+
}
|
|
159
|
+
/** Resolve a normalized match to the original event used by history deep links. */
|
|
160
|
+
matchEventId(messageKey, match, fallback) {
|
|
161
|
+
const fragments = this.db
|
|
162
|
+
.prepare(`SELECT f.event_id, f.text FROM search_fragments f JOIN ws_events e ON e.id = f.event_id
|
|
163
|
+
WHERE f.message_key = ? ORDER BY e.rowid`)
|
|
164
|
+
.all(messageKey);
|
|
165
|
+
if (fragments.length < 2)
|
|
166
|
+
return fragments[0]?.event_id ?? fallback;
|
|
167
|
+
// Normalize whole graphemes: NFC can compose characters across fragment
|
|
168
|
+
// boundaries, and lowercasing can expand them (İ → i + combining dot).
|
|
169
|
+
const source = fragments.map((fragment) => fragment.text).join('');
|
|
170
|
+
let normalizedEnd = 0;
|
|
171
|
+
let sourceOffset = 0;
|
|
172
|
+
for (const { segment, index } of graphemes.segment(source)) {
|
|
173
|
+
normalizedEnd += normalizeSearchText(segment).length;
|
|
174
|
+
if (normalizedEnd > match) {
|
|
175
|
+
sourceOffset = index;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
let fragmentEnd = 0;
|
|
180
|
+
for (const fragment of fragments) {
|
|
181
|
+
fragmentEnd += fragment.text.length;
|
|
182
|
+
if (fragmentEnd > sourceOffset)
|
|
183
|
+
return fragment.event_id;
|
|
184
|
+
}
|
|
185
|
+
return fallback;
|
|
186
|
+
}
|
|
187
|
+
async search(query, options = {}, cancelled = () => false) {
|
|
188
|
+
this.removeDeleted();
|
|
189
|
+
const needle = normalizeSearchText(query.trim());
|
|
190
|
+
if (!needle)
|
|
191
|
+
return [];
|
|
192
|
+
const trigram = [...needle].length >= 3 && !needle.includes('\0');
|
|
193
|
+
const clauses = [options.includeArchived ? '1' : 'w.archived_at IS NULL'];
|
|
194
|
+
const args = [];
|
|
195
|
+
if (trigram) {
|
|
196
|
+
clauses.push('search_messages_fts MATCH ?');
|
|
197
|
+
args.push(`"${needle.replaceAll('"', '""')}"`);
|
|
198
|
+
}
|
|
199
|
+
if (options.workspaceId) {
|
|
200
|
+
clauses.push('m.workspace_id = ?');
|
|
201
|
+
args.push(options.workspaceId);
|
|
202
|
+
}
|
|
203
|
+
const statement = this.db.prepare(`SELECT m.*, w.name AS workspace_name, w.archived_at
|
|
204
|
+
FROM search_messages m ${trigram ? 'JOIN search_messages_fts ON search_messages_fts.rowid = m.id' : ''}
|
|
205
|
+
JOIN workspaces w ON w.id = m.workspace_id WHERE ${clauses.join(' AND ')} ORDER BY m.created_at DESC, m.event_id DESC`);
|
|
206
|
+
const results = [];
|
|
207
|
+
let scanned = 0;
|
|
208
|
+
const limit = Math.min(200, Math.max(1, options.limit ?? 50));
|
|
209
|
+
for (const row of statement.iterate(...args)) {
|
|
210
|
+
if (cancelled())
|
|
211
|
+
throw new Error('Search cancelled');
|
|
212
|
+
const index = row.normalized.indexOf(needle);
|
|
213
|
+
if (index >= 0) {
|
|
214
|
+
results.push({
|
|
215
|
+
eventId: row.type === 'agent:event' ? this.matchEventId(row.message_key, index, row.event_id) : row.event_id,
|
|
216
|
+
sessionId: row.session_id,
|
|
217
|
+
workspaceId: row.workspace_id,
|
|
218
|
+
workspaceName: row.workspace_name,
|
|
219
|
+
archived: row.archived_at !== null,
|
|
220
|
+
type: row.type,
|
|
221
|
+
timestamp: row.created_at,
|
|
222
|
+
snippet: snippet(row.text, index, needle.length),
|
|
223
|
+
});
|
|
224
|
+
if (results.length >= limit)
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
if (++scanned % 500 === 0)
|
|
228
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
229
|
+
}
|
|
230
|
+
return results;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import { SearchIndexer } from './indexer.js';
|
|
4
|
+
const port = parentPort;
|
|
5
|
+
const db = new Database(workerData.dbPath);
|
|
6
|
+
db.pragma('journal_mode=WAL');
|
|
7
|
+
db.pragma('foreign_keys=ON');
|
|
8
|
+
db.pragma('busy_timeout=5000');
|
|
9
|
+
const indexer = new SearchIndexer(db);
|
|
10
|
+
const cancelled = new Set();
|
|
11
|
+
const requests = new Set();
|
|
12
|
+
let queue = Promise.resolve();
|
|
13
|
+
let stopped = false;
|
|
14
|
+
let timer;
|
|
15
|
+
function report() {
|
|
16
|
+
port.postMessage({ status: indexer.status() });
|
|
17
|
+
}
|
|
18
|
+
function pump() {
|
|
19
|
+
if (stopped)
|
|
20
|
+
return;
|
|
21
|
+
queue = queue
|
|
22
|
+
.then(() => {
|
|
23
|
+
if (stopped)
|
|
24
|
+
return;
|
|
25
|
+
indexer.tick();
|
|
26
|
+
report();
|
|
27
|
+
})
|
|
28
|
+
.catch((err) => {
|
|
29
|
+
// A writer may outlast busy_timeout. The rolled-back batch remains queued
|
|
30
|
+
// in SQLite; retry it on the next pump instead of poisoning this worker.
|
|
31
|
+
if (err?.code === 'SQLITE_BUSY' || err?.code?.startsWith('SQLITE_BUSY_'))
|
|
32
|
+
return;
|
|
33
|
+
stopped = true;
|
|
34
|
+
port.postMessage({ status: { state: 'error', processed: 0, total: 0, error: String(err) } });
|
|
35
|
+
})
|
|
36
|
+
.finally(() => {
|
|
37
|
+
if (!stopped)
|
|
38
|
+
timer = setTimeout(pump, indexer.status().state === 'building' ? 10 : 200);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
port.on('message', (message) => {
|
|
42
|
+
if (message.cancel) {
|
|
43
|
+
if (requests.has(message.id))
|
|
44
|
+
cancelled.add(message.id);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (message.stop) {
|
|
48
|
+
stopped = true;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
void queue.finally(() => {
|
|
51
|
+
db.close();
|
|
52
|
+
port.close();
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
requests.add(message.id);
|
|
57
|
+
queue = queue
|
|
58
|
+
.then(async () => {
|
|
59
|
+
if (stopped)
|
|
60
|
+
throw new Error('Search index unavailable');
|
|
61
|
+
if (cancelled.has(message.id))
|
|
62
|
+
return;
|
|
63
|
+
indexer.tick();
|
|
64
|
+
const results = await indexer.search(message.query ?? '', message.options, () => cancelled.has(message.id));
|
|
65
|
+
report();
|
|
66
|
+
port.postMessage({ id: message.id, results });
|
|
67
|
+
})
|
|
68
|
+
.catch((err) => {
|
|
69
|
+
port.postMessage({ id: message.id, error: err instanceof Error ? err.message : String(err) });
|
|
70
|
+
})
|
|
71
|
+
.finally(() => {
|
|
72
|
+
cancelled.delete(message.id);
|
|
73
|
+
requests.delete(message.id);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
pump();
|