@tacuchi/agent-workflow-cli 22.4.2 → 24.0.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 +39 -3
- package/dist/adapters/git-cli.js +59 -17
- package/dist/adapters/git-cli.js.map +1 -1
- package/dist/adapters/node-file-system.js +55 -1
- package/dist/adapters/node-file-system.js.map +1 -1
- package/dist/adapters/postgres-readonly-tools.js +277 -0
- package/dist/adapters/postgres-readonly-tools.js.map +1 -0
- package/dist/application/artifacts-service.js +2 -2
- package/dist/application/artifacts-service.js.map +1 -1
- package/dist/application/capability/dispatcher.js +2 -2
- package/dist/application/capability/dispatcher.js.map +1 -1
- package/dist/application/capability/installed-inventory.js +2 -2
- package/dist/application/capability/installed-inventory.js.map +1 -1
- package/dist/application/capability/readiness.js +1 -1
- package/dist/application/capability/readiness.js.map +1 -1
- package/dist/application/check-branch-service.js +2 -2
- package/dist/application/check-branch-service.js.map +1 -1
- package/dist/application/checkpoint/files-touched.js +271 -0
- package/dist/application/checkpoint/files-touched.js.map +1 -0
- package/dist/application/checkpoint/markdown.js +61 -8
- package/dist/application/checkpoint/markdown.js.map +1 -1
- package/dist/application/checkpoint/state-reader.js +8 -2
- package/dist/application/checkpoint/state-reader.js.map +1 -1
- package/dist/application/checkpoint-write-service.js +16 -5
- package/dist/application/checkpoint-write-service.js.map +1 -1
- package/dist/application/claims-ledger.js +330 -0
- package/dist/application/claims-ledger.js.map +1 -0
- package/dist/application/claims-recovery.js +338 -0
- package/dist/application/claims-recovery.js.map +1 -0
- package/dist/application/database-mcp-server.js +168 -0
- package/dist/application/database-mcp-server.js.map +1 -0
- package/dist/application/database-mcp-stdio.js +137 -0
- package/dist/application/database-mcp-stdio.js.map +1 -0
- package/dist/application/database-tool-catalog.js +226 -0
- package/dist/application/database-tool-catalog.js.map +1 -0
- package/dist/application/dev-only-services.js +154 -44
- package/dist/application/dev-only-services.js.map +1 -1
- package/dist/application/dsn-reader-service.js +1 -1
- package/dist/application/elicitation-server.js +382 -0
- package/dist/application/elicitation-server.js.map +1 -0
- package/dist/application/elicitation-stdio.js +48 -0
- package/dist/application/elicitation-stdio.js.map +1 -0
- package/dist/application/flow/advance.js +86 -5
- package/dist/application/flow/advance.js.map +1 -1
- package/dist/application/flow/checkout-observation.js +152 -0
- package/dist/application/flow/checkout-observation.js.map +1 -0
- package/dist/application/flow/flow-service.js +72 -13
- package/dist/application/flow/flow-service.js.map +1 -1
- package/dist/application/flow/internal-actions.js +460 -1
- package/dist/application/flow/internal-actions.js.map +1 -1
- package/dist/application/flow/internal-drive.js +22 -9
- package/dist/application/flow/internal-drive.js.map +1 -1
- package/dist/application/flow/prove.js +205 -0
- package/dist/application/flow/prove.js.map +1 -0
- package/dist/application/flow/run-journey.js +15 -0
- package/dist/application/flow/run-journey.js.map +1 -0
- package/dist/application/flow/run-projection.js +15 -3
- package/dist/application/flow/run-projection.js.map +1 -1
- package/dist/application/flow/run-state-service.js +19 -4
- package/dist/application/flow/run-state-service.js.map +1 -1
- package/dist/application/flow/submit.js +250 -72
- package/dist/application/flow/submit.js.map +1 -1
- package/dist/application/generate-launch-service.js +4 -4
- package/dist/application/generate-launch-service.js.map +1 -1
- package/dist/application/git-flow-service.js +9 -0
- package/dist/application/git-flow-service.js.map +1 -1
- package/dist/application/local-proposal.js +9 -0
- package/dist/application/local-proposal.js.map +1 -1
- package/dist/application/lock-service.js +96 -28
- package/dist/application/lock-service.js.map +1 -1
- package/dist/application/logging/log-events.js +19 -3
- package/dist/application/logging/log-events.js.map +1 -1
- package/dist/application/logging/logger.js +2 -2
- package/dist/application/logging/logger.js.map +1 -1
- package/dist/application/mcp-connections-service.js +115 -41
- package/dist/application/mcp-connections-service.js.map +1 -1
- package/dist/application/mcp-doctor-service.js +98 -54
- package/dist/application/mcp-doctor-service.js.map +1 -1
- package/dist/application/mcp-entry-classification.js +60 -0
- package/dist/application/mcp-entry-classification.js.map +1 -0
- package/dist/application/mcp-host-reader.js +114 -81
- package/dist/application/mcp-host-reader.js.map +1 -1
- package/dist/application/mcp-host-receipt-service.js +399 -0
- package/dist/application/mcp-host-receipt-service.js.map +1 -0
- package/dist/application/mcp-host-receipt-store.js +122 -0
- package/dist/application/mcp-host-receipt-store.js.map +1 -0
- package/dist/application/mcp-host-receipts.js +28 -0
- package/dist/application/mcp-host-receipts.js.map +1 -0
- package/dist/application/mcp-host-writer.js +321 -126
- package/dist/application/mcp-host-writer.js.map +1 -1
- package/dist/application/mcp-launch-probe-service.js +142 -0
- package/dist/application/mcp-launch-probe-service.js.map +1 -0
- package/dist/application/mcp-migration-service.js +351 -0
- package/dist/application/mcp-migration-service.js.map +1 -0
- package/dist/application/mcp-native-host-check-service.js +154 -0
- package/dist/application/mcp-native-host-check-service.js.map +1 -0
- package/dist/application/mcp-receipt-registration-service.js +105 -0
- package/dist/application/mcp-receipt-registration-service.js.map +1 -0
- package/dist/application/mcp-receipt-removal-service.js +62 -0
- package/dist/application/mcp-receipt-removal-service.js.map +1 -0
- package/dist/application/mcp-remove-service.js +84 -21
- package/dist/application/mcp-remove-service.js.map +1 -1
- package/dist/application/mcp-scope-common.js +2 -4
- package/dist/application/mcp-scope-common.js.map +1 -1
- package/dist/application/mcp-setup-service.js +60 -21
- package/dist/application/mcp-setup-service.js.map +1 -1
- package/dist/application/mcp-stdio-probe.js +526 -0
- package/dist/application/mcp-stdio-probe.js.map +1 -0
- package/dist/application/mcp-test-connection-service.js +23 -50
- package/dist/application/mcp-test-connection-service.js.map +1 -1
- package/dist/application/merge-state-service.js +10 -16
- package/dist/application/merge-state-service.js.map +1 -1
- package/dist/application/multiroot/claude.js +34 -15
- package/dist/application/multiroot/claude.js.map +1 -1
- package/dist/application/multiroot/codex.js +9 -5
- package/dist/application/multiroot/codex.js.map +1 -1
- package/dist/application/multiroot-service.js +20 -11
- package/dist/application/multiroot-service.js.map +1 -1
- package/dist/application/parsers/project-block.js +25 -2
- package/dist/application/parsers/project-block.js.map +1 -1
- package/dist/application/paths-service.js +30 -31
- package/dist/application/paths-service.js.map +1 -1
- package/dist/application/plan-exec-batch-service.js +549 -0
- package/dist/application/plan-exec-batch-service.js.map +1 -0
- package/dist/application/plan-exec-decision-service.js +273 -0
- package/dist/application/plan-exec-decision-service.js.map +1 -0
- package/dist/application/project-md-service.js +2 -2
- package/dist/application/project-md-service.js.map +1 -1
- package/dist/application/project-md-upsert-service.js +4 -4
- package/dist/application/project-md-upsert-service.js.map +1 -1
- package/dist/application/project-tab-data.js +12 -7
- package/dist/application/project-tab-data.js.map +1 -1
- package/dist/application/release-data-service.js +2 -2
- package/dist/application/release-data-service.js.map +1 -1
- package/dist/application/resume-service.js +32 -11
- package/dist/application/resume-service.js.map +1 -1
- package/dist/application/retirement/apply.js +84 -0
- package/dist/application/retirement/apply.js.map +1 -1
- package/dist/application/retirement/attribution.js +31 -1
- package/dist/application/retirement/attribution.js.map +1 -1
- package/dist/application/retirement/graph.js +19 -1
- package/dist/application/retirement/graph.js.map +1 -1
- package/dist/application/retirement/journal.js +11 -0
- package/dist/application/retirement/journal.js.map +1 -1
- package/dist/application/retirement/prepare.js +89 -3
- package/dist/application/retirement/prepare.js.map +1 -1
- package/dist/application/retirement/preview.js +16 -0
- package/dist/application/retirement/preview.js.map +1 -1
- package/dist/application/retirement/resolve.js +65 -0
- package/dist/application/retirement/resolve.js.map +1 -1
- package/dist/application/self/host-states.js +2 -0
- package/dist/application/self/host-states.js.map +1 -1
- package/dist/application/self/install-skill.js +21 -17
- package/dist/application/self/install-skill.js.map +1 -1
- package/dist/application/self/mcp-config.js +392 -84
- package/dist/application/self/mcp-config.js.map +1 -1
- package/dist/application/self/mcp-via-state.js +62 -0
- package/dist/application/self/mcp-via-state.js.map +1 -0
- package/dist/application/self/uninstall.js +15 -5
- package/dist/application/self/uninstall.js.map +1 -1
- package/dist/application/session-close-service.js +24 -1
- package/dist/application/session-close-service.js.map +1 -1
- package/dist/application/session-create-service.js +7 -0
- package/dist/application/session-create-service.js.map +1 -1
- package/dist/application/session-resume-service.js +2 -2
- package/dist/application/session-resume-service.js.map +1 -1
- package/dist/application/sessions-service.js +2 -4
- package/dist/application/sessions-service.js.map +1 -1
- package/dist/application/skills-resolver-service.js +4 -4
- package/dist/application/skills-resolver-service.js.map +1 -1
- package/dist/application/source-boundary-policy.js +18 -7
- package/dist/application/source-boundary-policy.js.map +1 -1
- package/dist/application/source-remove-service.js +12 -1
- package/dist/application/source-remove-service.js.map +1 -1
- package/dist/application/sources-service.js +2 -2
- package/dist/application/sources-service.js.map +1 -1
- package/dist/application/status-service.js +4 -0
- package/dist/application/status-service.js.map +1 -1
- package/dist/application/visibility-doctor-service.js +1 -1
- package/dist/application/visibility-doctor-service.js.map +1 -1
- package/dist/application/workline-index-service.js +182 -27
- package/dist/application/workline-index-service.js.map +1 -1
- package/dist/application/workspace-init-service.js +89 -271
- package/dist/application/workspace-init-service.js.map +1 -1
- package/dist/application/workspace-materialization-service.js +300 -0
- package/dist/application/workspace-materialization-service.js.map +1 -0
- package/dist/application/worktree-service.js +32 -3
- package/dist/application/worktree-service.js.map +1 -1
- package/dist/cli/commands/capability.js +4 -7
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/claims.js +72 -0
- package/dist/cli/commands/claims.js.map +1 -0
- package/dist/cli/commands/dev-only.js +73 -25
- package/dist/cli/commands/dev-only.js.map +1 -1
- package/dist/cli/commands/flow.js +145 -42
- package/dist/cli/commands/flow.js.map +1 -1
- package/dist/cli/commands/index.js +4 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/mcp.js +752 -76
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/multiroot.js +40 -7
- package/dist/cli/commands/multiroot.js.map +1 -1
- package/dist/cli/commands/resume.js +9 -3
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/retirement.js +9 -0
- package/dist/cli/commands/retirement.js.map +1 -1
- package/dist/cli/commands/session-create.js +1 -1
- package/dist/cli/commands/session-create.js.map +1 -1
- package/dist/cli/commands/skills.js +1 -1
- package/dist/cli/commands/skills.js.map +1 -1
- package/dist/cli/commands/status.js +72 -24
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/tool.js +141 -0
- package/dist/cli/commands/tool.js.map +1 -0
- package/dist/cli/commands/workspace-init.js +5 -5
- package/dist/cli/commands/workspace-init.js.map +1 -1
- package/dist/cli/help-groups.js +6 -1
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/main.js +272 -67
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/parser.js +8 -1
- package/dist/cli/parser.js.map +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli/render.js +15 -2
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/tui/app.js +3 -2
- package/dist/cli/tui/app.js.map +1 -1
- package/dist/cli/tui/components/workspace-init-form.js +7 -7
- package/dist/cli/tui/components/workspace-init-form.js.map +1 -1
- package/dist/cli/tui/data/workflow-content.js +2 -2
- package/dist/cli/tui/data/workflow-content.js.map +1 -1
- package/dist/cli/tui/tabs/config-tab.js +4 -3
- package/dist/cli/tui/tabs/config-tab.js.map +1 -1
- package/dist/cli/tui/tabs/mcp-tab-helpers.js +72 -1
- package/dist/cli/tui/tabs/mcp-tab-helpers.js.map +1 -1
- package/dist/cli/tui/tabs/mcp-tab.js +101 -42
- package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
- package/dist/cli/tui/tabs/project-tab.js +18 -21
- package/dist/cli/tui/tabs/project-tab.js.map +1 -1
- package/dist/cli/tui/workspace-root.js +13 -0
- package/dist/cli/tui/workspace-root.js.map +1 -0
- package/dist/domain/capability/protocol.js +2 -2
- package/dist/domain/capability/protocol.js.map +1 -1
- package/dist/domain/database-tools.js +445 -0
- package/dist/domain/database-tools.js.map +1 -0
- package/dist/domain/degradation-notice.js +60 -0
- package/dist/domain/degradation-notice.js.map +1 -0
- package/dist/domain/elicitation.js +152 -0
- package/dist/domain/elicitation.js.map +1 -0
- package/dist/domain/flow/answer.js +173 -42
- package/dist/domain/flow/answer.js.map +1 -1
- package/dist/domain/flow/authority.js +184 -127
- package/dist/domain/flow/authority.js.map +1 -1
- package/dist/domain/flow/directive.js +47 -1
- package/dist/domain/flow/directive.js.map +1 -1
- package/dist/domain/flow/rules.js +7 -4
- package/dist/domain/flow/rules.js.map +1 -1
- package/dist/domain/flow/run-state.js +636 -31
- package/dist/domain/flow/run-state.js.map +1 -1
- package/dist/domain/harnesses.js +16 -0
- package/dist/domain/harnesses.js.map +1 -1
- package/dist/domain/mcp-entry.js +164 -28
- package/dist/domain/mcp-entry.js.map +1 -1
- package/dist/domain/postgres-object-search.js +320 -0
- package/dist/domain/postgres-object-search.js.map +1 -0
- package/dist/domain/redaction.js +47 -0
- package/dist/domain/redaction.js.map +1 -0
- package/dist/domain/retirement/proposal.js +1 -0
- package/dist/domain/retirement/proposal.js.map +1 -1
- package/dist/domain/structured-choice-stamp.js +32 -4
- package/dist/domain/structured-choice-stamp.js.map +1 -1
- package/dist/domain/workline-mcp-entry.js +34 -0
- package/dist/domain/workline-mcp-entry.js.map +1 -0
- package/dist/ports/postgres-tools.js +10 -0
- package/dist/ports/postgres-tools.js.map +1 -0
- package/dist/runtime/namespace-resolver.js +104 -18
- package/dist/runtime/namespace-resolver.js.map +1 -1
- package/package.json +5 -1
- package/skills/w/README.md +2 -2
- package/skills/w/SKILL.md +9 -10
- package/skills/w/artifacts/README.md +1 -1
- package/skills/w/commands/README.md +3 -3
- package/skills/w/commands/persist.md +3 -1
- package/skills/w/commands/resume.md +1 -1
- package/skills/w/commands/spec-new.md +2 -2
- package/skills/w/commands/status.md +1 -1
- package/skills/w/commands/workspace-init.md +9 -10
- package/skills/w/context/MANIFEST.json +1 -4
- package/skills/w/harness/HARNESS.md +1 -1
- package/skills/w/loops/quick-loop/LOOP.md +1 -1
- package/skills/w/modules/PROMPT-CONTINUITY.md +1 -1
- package/skills/w/modules/WORKSPACE-SCAFFOLD.md +17 -20
- package/skills/w/schemas/capability-descriptor.schema.json +1 -1
- package/dist/application/mcp-dbhub-launcher.js +0 -216
- package/dist/application/mcp-dbhub-launcher.js.map +0 -1
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovering a reservation whose owner never came back.
|
|
3
|
+
*
|
|
4
|
+
* The normal cycle needs none of this: completing, closing and cancelling all
|
|
5
|
+
* resolve a reservation through its owner. What is left is the run that simply
|
|
6
|
+
* died — no publication, no close, nothing terminal — and left a correlative held
|
|
7
|
+
* by a session that is never going to finish it. There is deliberately NO clock
|
|
8
|
+
* here: a reservation does not expire, because "it has been a while" is not
|
|
9
|
+
* evidence that nobody is coming, and a timer that frees a slot somebody is still
|
|
10
|
+
* writing into would be worse than the stranded number it fixes.
|
|
11
|
+
*
|
|
12
|
+
* So the release is explicit, authorized, and ORDERED. The order is the whole
|
|
13
|
+
* safety argument and it runs one way only:
|
|
14
|
+
*
|
|
15
|
+
* 1. the reservation is still exactly its own intact marker — otherwise the
|
|
16
|
+
* bytes are somebody's work and this is not a recovery;
|
|
17
|
+
* 2. a revocation is SEALED, durably and irrevocably, scoped to that one claim;
|
|
18
|
+
* 3. and only then is the slot released.
|
|
19
|
+
*
|
|
20
|
+
* Step 2 before step 3 is not a preference. Once the file is gone the correlative
|
|
21
|
+
* is eligible again, so a late sealed proposal from the dead owner — one that was
|
|
22
|
+
* approved before the recovery and lands after it — would write a document onto a
|
|
23
|
+
* number somebody else may already hold. The fence is what makes that publication
|
|
24
|
+
* fail instead of collide, and a fence written after the release is a window. If
|
|
25
|
+
* the seal cannot be written, the recovery FAILS and releases nothing: a released
|
|
26
|
+
* slot with no fence is the one outcome this must never produce.
|
|
27
|
+
*
|
|
28
|
+
* The revocation belongs to the CLAIM, never to the session. The owner may be
|
|
29
|
+
* alive and working on other things, and revoking its whole session to reclaim
|
|
30
|
+
* one number would destroy work to tidy up a correlative.
|
|
31
|
+
*/
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { leadingCorrelative } from "../domain/correlative.js";
|
|
34
|
+
import { reservationOwnerOf } from "../domain/reservation.js";
|
|
35
|
+
import { appendClaimEvent, claimKey, isRevoked, openOwnerOfSlot, readClaimEvents, wasPublished, } from "./claims-ledger.js";
|
|
36
|
+
import { withCwdLock } from "./lock-service.js";
|
|
37
|
+
import { semanticDigest } from "./semantic-operation/protocol.js";
|
|
38
|
+
import { CLOSED_MARKER, listSessionFolders } from "./session-resolver.js";
|
|
39
|
+
/** The identity a reservation's slot maps to, or `null` for an unattributable one. */
|
|
40
|
+
function claimOfSlot(slot) {
|
|
41
|
+
if (slot.owner === null)
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
category: slot.category,
|
|
45
|
+
correlative: slot.correlative,
|
|
46
|
+
name: slot.name,
|
|
47
|
+
owner: slot.owner,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The slot a numbered file is, or `null` when it is a document.
|
|
52
|
+
*
|
|
53
|
+
* The LEDGER decides before the bytes do, and the order is the fix for a real
|
|
54
|
+
* defect: classifying from bytes alone made every empty numbered file an
|
|
55
|
+
* ownerless legacy placeholder — releasable with no fence — including a published
|
|
56
|
+
* document whose content happens to be empty, and including a reservation whose
|
|
57
|
+
* marker somebody emptied. In the first case a recovery deleted a published
|
|
58
|
+
* document; in the second it gave a correlative back while its real owner still
|
|
59
|
+
* held an open claim on it. Both times the record held the answer and only the
|
|
60
|
+
* bytes were consulted.
|
|
61
|
+
*/
|
|
62
|
+
function slotOf(category, fileName, text, events, activeOwners) {
|
|
63
|
+
const correlative = leadingCorrelative(fileName);
|
|
64
|
+
if (correlative === null)
|
|
65
|
+
return null;
|
|
66
|
+
const name = fileName.slice(correlative.length + 1);
|
|
67
|
+
const slot = { category, correlative, name };
|
|
68
|
+
// Published is terminal and terminal means "a document lives here": whatever
|
|
69
|
+
// its bytes look like, it is not a slot anybody may free.
|
|
70
|
+
if (wasPublished(events, slot))
|
|
71
|
+
return null;
|
|
72
|
+
const markerOwner = reservationOwnerOf(text);
|
|
73
|
+
const base = { path: `docs/${category}/${fileName}`, category, correlative, name };
|
|
74
|
+
if (markerOwner !== null) {
|
|
75
|
+
return {
|
|
76
|
+
...base,
|
|
77
|
+
kind: "reservation",
|
|
78
|
+
owner: markerOwner,
|
|
79
|
+
ownerActive: activeOwners.has(markerOwner),
|
|
80
|
+
revoked: false,
|
|
81
|
+
intact: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Real content and no marker: a document, by its bytes this time.
|
|
85
|
+
if (text.trim().length > 0)
|
|
86
|
+
return null;
|
|
87
|
+
// Empty. If the ledger still holds an open claim on this slot, it is that
|
|
88
|
+
// owner's reservation with damaged bytes — not nobody's placeholder.
|
|
89
|
+
const open = openOwnerOfSlot(events, slot);
|
|
90
|
+
if (open !== null) {
|
|
91
|
+
return {
|
|
92
|
+
...base,
|
|
93
|
+
kind: "reservation",
|
|
94
|
+
owner: open.owner,
|
|
95
|
+
ownerActive: activeOwners.has(open.owner),
|
|
96
|
+
revoked: false,
|
|
97
|
+
intact: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
...base,
|
|
102
|
+
kind: "legacy-placeholder",
|
|
103
|
+
owner: null,
|
|
104
|
+
ownerActive: null,
|
|
105
|
+
revoked: false,
|
|
106
|
+
intact: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Walk `docs/` into `into`, throwing on the first unreadable thing.
|
|
111
|
+
*
|
|
112
|
+
* The accumulator is the caller's so a failure still reports what it managed to
|
|
113
|
+
* see: an unreadable category is a different fact from an empty one, and folding
|
|
114
|
+
* the two would let the board say "no reservations" about a directory nobody
|
|
115
|
+
* could open.
|
|
116
|
+
*/
|
|
117
|
+
async function walkSlots(fs, docs, events, activeOwners, into) {
|
|
118
|
+
for (const category of await fs.list(docs)) {
|
|
119
|
+
if (category.type !== "dir")
|
|
120
|
+
continue;
|
|
121
|
+
for (const entry of await fs.list(category.path)) {
|
|
122
|
+
// The filename decides whether the bytes are worth reading at all. Reading
|
|
123
|
+
// every file in every docs/ subdirectory to then discard most of them on the
|
|
124
|
+
// first line of `slotOf` made a scan that runs on EVERY board projection pay
|
|
125
|
+
// for the whole corpus.
|
|
126
|
+
if (entry.type !== "file" || leadingCorrelative(entry.name) === null)
|
|
127
|
+
continue;
|
|
128
|
+
const slot = slotOf(category.name, entry.name, await fs.readText(entry.path), events, activeOwners);
|
|
129
|
+
if (slot === null)
|
|
130
|
+
continue;
|
|
131
|
+
const claim = claimOfSlot(slot);
|
|
132
|
+
slot.revoked = claim !== null && isRevoked(events, claim);
|
|
133
|
+
into.push(slot);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The session folders that are still open.
|
|
139
|
+
*
|
|
140
|
+
* Read once per scan, because the sanctioned action for a slot depends on it: a
|
|
141
|
+
* reservation of a live session is that session's to finish or close, and only a
|
|
142
|
+
* slot nobody is finishing may be recovered.
|
|
143
|
+
*/
|
|
144
|
+
async function activeSessionFolders(fs, paths) {
|
|
145
|
+
const active = new Set();
|
|
146
|
+
try {
|
|
147
|
+
for (const folder of await listSessionFolders(fs, paths.cwdSessionsDir())) {
|
|
148
|
+
if (await fs.exists(join(folder.path, CLOSED_MARKER)))
|
|
149
|
+
continue;
|
|
150
|
+
active.add(folder.name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// An unreadable sessions dir means nobody can be proven alive. The fallback is
|
|
155
|
+
// the conservative one: with no owner known to be active, no slot is offered
|
|
156
|
+
// for recovery on the strength of liveness it could not check.
|
|
157
|
+
}
|
|
158
|
+
return active;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Every reservation and legacy placeholder under `docs/`.
|
|
162
|
+
*
|
|
163
|
+
* Walks each immediate subdirectory rather than a list of categories, for the
|
|
164
|
+
* same reason the close's sweep does: the claim mechanism is category-agnostic,
|
|
165
|
+
* and a hardcoded list is a second place to update the day something else claims
|
|
166
|
+
* a number.
|
|
167
|
+
*
|
|
168
|
+
* A failure comes back WITH its reason and with whatever was already seen. An
|
|
169
|
+
* unreadable `docs/` is not an empty one, and swallowing the difference would let
|
|
170
|
+
* the board answer "no reservations" about a directory nobody could open.
|
|
171
|
+
*/
|
|
172
|
+
export async function scanSlots(fs, paths) {
|
|
173
|
+
const docs = join(paths.workspaceDir(), "docs");
|
|
174
|
+
const slots = [];
|
|
175
|
+
const ledger = await readClaimEvents(fs, paths);
|
|
176
|
+
const activeOwners = await activeSessionFolders(fs, paths);
|
|
177
|
+
const sorted = () => slots.sort((a, b) => a.path.localeCompare(b.path));
|
|
178
|
+
if (!(await fs.exists(docs)))
|
|
179
|
+
return { slots };
|
|
180
|
+
try {
|
|
181
|
+
await walkSlots(fs, docs, ledger.events, activeOwners, slots);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
slots: sorted(),
|
|
186
|
+
error: `no se pudo revisar docs/ en busca de reservas: ${error instanceof Error ? error.message : String(error)}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { slots: sorted() };
|
|
190
|
+
}
|
|
191
|
+
function sealRecovery(body) {
|
|
192
|
+
return { ...body, digest: semanticDigest(body) };
|
|
193
|
+
}
|
|
194
|
+
/** The proposal for one slot, or why there is none. */
|
|
195
|
+
export async function previewRecovery(fs, paths, target) {
|
|
196
|
+
const scan = await scanSlots(fs, paths);
|
|
197
|
+
const slot = scan.slots.find((candidate) => candidate.path === target);
|
|
198
|
+
if (slot === undefined) {
|
|
199
|
+
return {
|
|
200
|
+
error: `'${target}' no es una reserva ni un placeholder legacy de este workspace`,
|
|
201
|
+
action: scan.error ??
|
|
202
|
+
"corré 'aw claims' para ver los correlativos recuperables; un documento publicado no se recupera",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
proposal: sealRecovery({
|
|
207
|
+
version: 1,
|
|
208
|
+
target: slot.path,
|
|
209
|
+
kind: slot.kind,
|
|
210
|
+
claim: claimOfSlot(slot),
|
|
211
|
+
requires_no_producer_confirmation: !slot.intact,
|
|
212
|
+
resuming: slot.revoked,
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Seal the fence, then release — and never the other way round.
|
|
218
|
+
*
|
|
219
|
+
* The whole thing runs under the workspace lock, which is not decoration: the
|
|
220
|
+
* check that the slot is still what the preview saw, the seal and the removal are
|
|
221
|
+
* three separate awaits, and without the lock a sanctioned publication could pass
|
|
222
|
+
* its own fence, take the lock, write its document and have this function delete
|
|
223
|
+
* it a moment later — leaving the correlative recorded as both spent-forever and
|
|
224
|
+
* eligible-again, with the published document silently gone. Its two siblings hold
|
|
225
|
+
* the lock across exactly this span for exactly this reason.
|
|
226
|
+
*
|
|
227
|
+
* It also re-derives its own proposal inside the lock instead of trusting the
|
|
228
|
+
* digest it was handed: the approval proves a person authorized THIS recovery, not
|
|
229
|
+
* that the world still looks the way it did when they read it.
|
|
230
|
+
*/
|
|
231
|
+
export async function applyRecovery(fs, paths, input) {
|
|
232
|
+
const outcome = await withCwdLock(fs, paths, () => recoverUnderLock(fs, paths, input));
|
|
233
|
+
if ("error" in outcome && "action" in outcome)
|
|
234
|
+
return outcome;
|
|
235
|
+
if ("error" in outcome) {
|
|
236
|
+
return {
|
|
237
|
+
error: `no se pudo tomar el candado del workspace: ${outcome.error}`,
|
|
238
|
+
action: "esperá a que otro flujo lo libere y volvé a aplicar la recuperación",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return outcome;
|
|
242
|
+
}
|
|
243
|
+
async function recoverUnderLock(fs, paths, input) {
|
|
244
|
+
const preview = await previewRecovery(fs, paths, input.target);
|
|
245
|
+
if ("error" in preview)
|
|
246
|
+
return preview;
|
|
247
|
+
const proposal = preview.proposal;
|
|
248
|
+
if (proposal.digest !== input.approval) {
|
|
249
|
+
return {
|
|
250
|
+
error: "la aprobación no corresponde a la recuperación vigente",
|
|
251
|
+
action: `volvé a mirarla con 'aw claims recover ${input.target}' y aprobá el digest que devuelve`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (proposal.requires_no_producer_confirmation && input.noProducerConfirmed !== true) {
|
|
255
|
+
return {
|
|
256
|
+
error: proposal.kind === "legacy-placeholder"
|
|
257
|
+
? `'${input.target}' es un placeholder legacy ambiguo: sus bytes no prueban que nadie vaya a escribirlo`
|
|
258
|
+
: `'${input.target}' ya no tiene el marcador intacto de su dueño: alguien escribió ahí`,
|
|
259
|
+
action: "confirmá explícitamente que no queda productor capaz de escribir ese destino antes de liberar su correlativo",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const claim = proposal.claim;
|
|
263
|
+
if (claim !== null && !proposal.resuming) {
|
|
264
|
+
// The fence FIRST. If this throws, nothing was released and the slot is
|
|
265
|
+
// exactly as it was: the recovery failed, which is the correct outcome.
|
|
266
|
+
// Releasing first would leave a window where the correlative is eligible and
|
|
267
|
+
// a late sealed publication can still land on it.
|
|
268
|
+
await appendClaimEvent(fs, paths, {
|
|
269
|
+
at: new Date().toISOString(),
|
|
270
|
+
event: "revoked",
|
|
271
|
+
claim,
|
|
272
|
+
cause: `aw claims recover: recuperación autorizada de ${proposal.target}`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// Recorded before the file goes, same reason as the close's release: a slot
|
|
276
|
+
// freed with no line saying so is the state this ledger exists to end. The
|
|
277
|
+
// identity is the SLOT's, so it joins to its own history by claimKey — an
|
|
278
|
+
// ownerless placeholder still records the category, correlative and name it
|
|
279
|
+
// gave back, and only its owner field says nobody held it.
|
|
280
|
+
await appendClaimEvent(fs, paths, {
|
|
281
|
+
at: new Date().toISOString(),
|
|
282
|
+
event: "released",
|
|
283
|
+
claim: claim ?? {
|
|
284
|
+
category: slotIdentityOf(proposal.target).category,
|
|
285
|
+
correlative: slotIdentityOf(proposal.target).correlative,
|
|
286
|
+
name: slotIdentityOf(proposal.target).name,
|
|
287
|
+
owner: LEGACY_OWNERLESS,
|
|
288
|
+
},
|
|
289
|
+
cause: claim === null
|
|
290
|
+
? "aw claims recover: placeholder legacy liberado con confirmación explícita de que no queda productor"
|
|
291
|
+
: `aw claims recover: liberado tras revocar ${claimKey(claim)}`,
|
|
292
|
+
});
|
|
293
|
+
await fs.remove(join(paths.workspaceDir(), proposal.target));
|
|
294
|
+
return {
|
|
295
|
+
applied: {
|
|
296
|
+
target: proposal.target,
|
|
297
|
+
revoked: claim,
|
|
298
|
+
released: true,
|
|
299
|
+
resumed: proposal.resuming,
|
|
300
|
+
digest: proposal.digest,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** The owner field of a slot that never had one. Never a session folder. */
|
|
305
|
+
const LEGACY_OWNERLESS = "(placeholder legacy sin dueño)";
|
|
306
|
+
/** `docs/<cat>/<NNN>-<name>` split the same way every ledger record splits it. */
|
|
307
|
+
function slotIdentityOf(target) {
|
|
308
|
+
const parts = target.split("/");
|
|
309
|
+
const category = parts[1] ?? "";
|
|
310
|
+
const file = parts[2] ?? "";
|
|
311
|
+
const correlative = leadingCorrelative(file) ?? "";
|
|
312
|
+
return {
|
|
313
|
+
category,
|
|
314
|
+
correlative,
|
|
315
|
+
name: correlative.length > 0 ? file.slice(correlative.length + 1) : file,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The one action this slot sanctions, named per slot.
|
|
320
|
+
*
|
|
321
|
+
* A reservation of a live session is resumed or closed by its owner; only a slot
|
|
322
|
+
* nobody is finishing gets recovered. And a slot whose bytes are not its own
|
|
323
|
+
* intact marker carries the confirmation flag in the command itself, so the
|
|
324
|
+
* operator sees the extra assertion being asked of them before they type it.
|
|
325
|
+
*/
|
|
326
|
+
export function sanctionedActionFor(slot) {
|
|
327
|
+
// A live owner's reservation is NOT a recovery candidate. Naming the recovery
|
|
328
|
+
// here was destructive: the board handed the running session the one command
|
|
329
|
+
// that revokes its own slot irrevocably, and the flow reads this very field as
|
|
330
|
+
// "the sanctioned next command". Closing the owner releases an intact
|
|
331
|
+
// reservation as part of closing, which is the action that actually resolves it.
|
|
332
|
+
if (slot.ownerActive === true && slot.owner !== null) {
|
|
333
|
+
return `aw session-close --code ${slot.owner}`;
|
|
334
|
+
}
|
|
335
|
+
const confirm = slot.intact ? "" : " --confirm-no-producer";
|
|
336
|
+
return `aw claims recover ${slot.path}${confirm}`;
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=claims-recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims-recovery.js","sourceRoot":"","sources":["../../src/application/claims-recovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { encodeToolResponse, toolFailure } from "../domain/database-tools.js";
|
|
2
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
3
|
+
/** JSON-RPC adapter over the shared catalog. It owns no tool rules. */
|
|
4
|
+
export function createDatabaseMcpServer(deps) {
|
|
5
|
+
let lifecycle = "new";
|
|
6
|
+
const activeCalls = new Map();
|
|
7
|
+
const cancelledBeforeStart = new Set();
|
|
8
|
+
function reply(id, result) {
|
|
9
|
+
if (!isJsonRpcResponseId(id))
|
|
10
|
+
return;
|
|
11
|
+
deps.send({ jsonrpc: "2.0", id, result });
|
|
12
|
+
}
|
|
13
|
+
function replyError(id, code, message) {
|
|
14
|
+
if (!isJsonRpcResponseId(id))
|
|
15
|
+
return;
|
|
16
|
+
deps.send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
17
|
+
}
|
|
18
|
+
function toolResult(response) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: encodeToolResponse(response), mimeType: "application/json" }],
|
|
21
|
+
...(response.success ? {} : { isError: true }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function dispatchToolCall(message) {
|
|
25
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
26
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
27
|
+
const requestKey = requestIdKey(message.id);
|
|
28
|
+
// tools/call is a request, not a notification. Rejecting an id-less call
|
|
29
|
+
// prevents an untracked database operation that the client cannot cancel.
|
|
30
|
+
if (requestKey === undefined) {
|
|
31
|
+
replyError(message.id, -32600, "tools/call requiere un id de solicitud válido");
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (cancelledBeforeStart.delete(requestKey)) {
|
|
35
|
+
reply(message.id, toolResult(toolFailure("QUERY_CANCELLED", "La consulta fue cancelada.")));
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
activeCalls.set(requestKey, controller);
|
|
40
|
+
try {
|
|
41
|
+
const outcome = await deps.catalog.call({
|
|
42
|
+
tool: name,
|
|
43
|
+
connection: deps.connection,
|
|
44
|
+
input: params.arguments,
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
});
|
|
47
|
+
reply(message.id, toolResult(outcome.response));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
reply(message.id, toolResult(toolFailure("TOOL_EXECUTION_FAILED", "La tool no pudo completar la solicitud.")));
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (activeCalls.get(requestKey) === controller) {
|
|
54
|
+
activeCalls.delete(requestKey);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
async function cancelToolCall(message) {
|
|
60
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
61
|
+
const key = requestIdKey(params.requestId);
|
|
62
|
+
if (key === undefined)
|
|
63
|
+
return true;
|
|
64
|
+
const active = activeCalls.get(key);
|
|
65
|
+
if (active !== undefined) {
|
|
66
|
+
active.abort();
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
// A stdio transport may receive cancellation while an earlier call keeps a
|
|
70
|
+
// later request queued. Retain bounded intent so that later call never
|
|
71
|
+
// reaches PostgreSQL once it starts.
|
|
72
|
+
cancelledBeforeStart.add(key);
|
|
73
|
+
if (cancelledBeforeStart.size > 1_000) {
|
|
74
|
+
const oldest = cancelledBeforeStart.values().next().value;
|
|
75
|
+
if (oldest !== undefined)
|
|
76
|
+
cancelledBeforeStart.delete(oldest);
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
async function initialize(message) {
|
|
81
|
+
if (lifecycle !== "new") {
|
|
82
|
+
replyError(message.id, -32600, "initialize sólo puede llamarse una vez por sesión");
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
lifecycle = "awaiting-initialized";
|
|
86
|
+
reply(message.id, {
|
|
87
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
88
|
+
capabilities: { tools: {} },
|
|
89
|
+
serverInfo: { name: "agent-workflow-db", version: "1" },
|
|
90
|
+
});
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
async function initialized() {
|
|
94
|
+
if (lifecycle === "awaiting-initialized") {
|
|
95
|
+
lifecycle = "ready";
|
|
96
|
+
await deps.onHostLoadObserved?.();
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
function readyOrReply(message) {
|
|
101
|
+
if (lifecycle === "ready")
|
|
102
|
+
return true;
|
|
103
|
+
replyError(message.id, -32002, "Server not initialized");
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
async function listTools(message) {
|
|
107
|
+
if (!readyOrReply(message))
|
|
108
|
+
return true;
|
|
109
|
+
try {
|
|
110
|
+
const list = deps.catalog.list(deps.connection);
|
|
111
|
+
reply(message.id, "tools" in list.payload ? list.payload : toolResult(list.payload));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
reply(message.id, toolResult(toolFailure("MCP_CONNECTION_INVALID", "No se pudo resolver la conexión solicitada.")));
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
async function callTool(message) {
|
|
119
|
+
if (!readyOrReply(message))
|
|
120
|
+
return true;
|
|
121
|
+
return await dispatchToolCall(message);
|
|
122
|
+
}
|
|
123
|
+
const handlers = {
|
|
124
|
+
initialize,
|
|
125
|
+
"notifications/initialized": async () => await initialized(),
|
|
126
|
+
"notifications/cancelled": async (message) => await cancelToolCall(message),
|
|
127
|
+
"tools/list": listTools,
|
|
128
|
+
"tools/call": callTool,
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
async handle(message) {
|
|
132
|
+
if (!isJsonRpcEnvelope(message))
|
|
133
|
+
return false;
|
|
134
|
+
const handler = handlers[message.method];
|
|
135
|
+
if (handler !== undefined)
|
|
136
|
+
return await handler(message);
|
|
137
|
+
replyError(message.id, -32601, "Method not found");
|
|
138
|
+
return true;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Accept only envelopes for which every response remains valid JSON-RPC. */
|
|
143
|
+
function isJsonRpcEnvelope(value) {
|
|
144
|
+
return (isRecord(value) &&
|
|
145
|
+
value.jsonrpc === "2.0" &&
|
|
146
|
+
typeof value.method === "string" &&
|
|
147
|
+
isJsonRpcId(value.id));
|
|
148
|
+
}
|
|
149
|
+
function isJsonRpcId(value) {
|
|
150
|
+
return value === undefined || isJsonRpcResponseId(value);
|
|
151
|
+
}
|
|
152
|
+
function isJsonRpcResponseId(value) {
|
|
153
|
+
return value === null || typeof value === "string" || isFiniteId(value);
|
|
154
|
+
}
|
|
155
|
+
function isFiniteId(value) {
|
|
156
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
157
|
+
}
|
|
158
|
+
function requestIdKey(value) {
|
|
159
|
+
if (typeof value === "string")
|
|
160
|
+
return `string:${value}`;
|
|
161
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
162
|
+
return `number:${value}`;
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
function isRecord(value) {
|
|
166
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=database-mcp-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database-mcp-server.js","sourceRoot":"","sources":["../../src/application/database-mcp-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { StringDecoder } from "node:string_decoder";
|
|
2
|
+
import { MAX_TOOL_TRANSPORT_INPUT_BYTES } from "../domain/database-tools.js";
|
|
3
|
+
import { createDatabaseMcpServer } from "./database-mcp-server.js";
|
|
4
|
+
// The RPC envelope is allowed a small fixed overhead beyond the canonical 1
|
|
5
|
+
// MiB tool input. The shared catalog still makes the exact input decision.
|
|
6
|
+
const MAX_MCP_INPUT_FRAME_BYTES = MAX_TOOL_TRANSPORT_INPUT_BYTES;
|
|
7
|
+
/**
|
|
8
|
+
* Line-framed stdio transport. stdout is exclusively JSON-RPC; malformed input
|
|
9
|
+
* is diagnosed on stderr and cannot corrupt a later valid request.
|
|
10
|
+
*/
|
|
11
|
+
export function runDatabaseMcpStdio(deps) {
|
|
12
|
+
const server = createDatabaseMcpServer({
|
|
13
|
+
send: (message) => deps.output.write(`${JSON.stringify(message)}\n`),
|
|
14
|
+
catalog: deps.catalog,
|
|
15
|
+
connection: deps.connection,
|
|
16
|
+
...(deps.onHostLoadObserved === undefined
|
|
17
|
+
? {}
|
|
18
|
+
: { onHostLoadObserved: deps.onHostLoadObserved }),
|
|
19
|
+
});
|
|
20
|
+
const frames = {
|
|
21
|
+
buffer: "",
|
|
22
|
+
bufferedBytes: 0,
|
|
23
|
+
discardingOversizedLine: false,
|
|
24
|
+
decoder: new StringDecoder("utf8"),
|
|
25
|
+
};
|
|
26
|
+
let queue = Promise.resolve();
|
|
27
|
+
function enqueue(line) {
|
|
28
|
+
let message;
|
|
29
|
+
try {
|
|
30
|
+
message = JSON.parse(line);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
deps.diagnostics.write("aw mcp serve-db: línea JSON-RPC inválida descartada\n");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Cancellation must not wait behind a running database call in the serial
|
|
37
|
+
// queue. All other frames retain order, including the lifecycle sequence.
|
|
38
|
+
if (isCancellationNotification(message)) {
|
|
39
|
+
void server.handle(message).catch(() => {
|
|
40
|
+
deps.diagnostics.write("aw mcp serve-db: línea JSON-RPC inválida descartada\n");
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
queue = queue.then(async () => {
|
|
45
|
+
try {
|
|
46
|
+
await server.handle(message);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
deps.diagnostics.write("aw mcp serve-db: línea JSON-RPC inválida descartada\n");
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
let ended = false;
|
|
55
|
+
const finish = () => {
|
|
56
|
+
if (ended)
|
|
57
|
+
return;
|
|
58
|
+
ended = true;
|
|
59
|
+
// An unterminated tail is not a JSON-RPC frame. Do not enqueue it, but
|
|
60
|
+
// flush the decoder so no multibyte bytes remain retained by the stream.
|
|
61
|
+
if (!frames.discardingOversizedLine)
|
|
62
|
+
frames.buffer += frames.decoder.end();
|
|
63
|
+
void queue.then(() => resolve());
|
|
64
|
+
};
|
|
65
|
+
deps.input.on("data", (chunk) => {
|
|
66
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
67
|
+
enqueueConsumption(consumeChunk(frames, bytes), deps, enqueue);
|
|
68
|
+
});
|
|
69
|
+
deps.input.on("end", finish);
|
|
70
|
+
deps.input.on("close", finish);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function isCancellationNotification(message) {
|
|
74
|
+
return (message !== null &&
|
|
75
|
+
typeof message === "object" &&
|
|
76
|
+
!Array.isArray(message) &&
|
|
77
|
+
message.method === "notifications/cancelled");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Splits raw bytes at newline boundaries before decoding. This matters for an
|
|
81
|
+
* oversized frame already terminated in the same chunk: appending/splitting a
|
|
82
|
+
* decoded 100 MiB string merely to reject it would defeat the input bound.
|
|
83
|
+
*/
|
|
84
|
+
function consumeChunk(state, bytes) {
|
|
85
|
+
const frames = [];
|
|
86
|
+
let oversizedFrames = 0;
|
|
87
|
+
let offset = 0;
|
|
88
|
+
while (offset < bytes.length) {
|
|
89
|
+
const newline = bytes.indexOf(0x0a, offset);
|
|
90
|
+
const end = newline < 0 ? bytes.length : newline + 1;
|
|
91
|
+
const segment = bytes.subarray(offset, end);
|
|
92
|
+
if (state.discardingOversizedLine) {
|
|
93
|
+
if (newline >= 0) {
|
|
94
|
+
state.discardingOversizedLine = false;
|
|
95
|
+
state.decoder = new StringDecoder("utf8");
|
|
96
|
+
}
|
|
97
|
+
offset = end;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (state.bufferedBytes + segment.length > MAX_MCP_INPUT_FRAME_BYTES) {
|
|
101
|
+
state.buffer = "";
|
|
102
|
+
state.bufferedBytes = 0;
|
|
103
|
+
state.decoder = new StringDecoder("utf8");
|
|
104
|
+
state.discardingOversizedLine = newline < 0;
|
|
105
|
+
oversizedFrames += 1;
|
|
106
|
+
offset = end;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
state.bufferedBytes += segment.length;
|
|
110
|
+
state.buffer += state.decoder.write(segment);
|
|
111
|
+
if (newline >= 0)
|
|
112
|
+
consumeCompleteLine(state, frames);
|
|
113
|
+
offset = end;
|
|
114
|
+
}
|
|
115
|
+
return { frames, oversizedFrames };
|
|
116
|
+
}
|
|
117
|
+
function consumeCompleteLine(state, frames) {
|
|
118
|
+
const newline = state.buffer.indexOf("\n");
|
|
119
|
+
if (newline < 0)
|
|
120
|
+
return;
|
|
121
|
+
const trimmed = state.buffer.slice(0, newline).replace(/\r$/, "").trim();
|
|
122
|
+
state.buffer = state.buffer.slice(newline + 1);
|
|
123
|
+
state.bufferedBytes = 0;
|
|
124
|
+
if (trimmed.length > 0)
|
|
125
|
+
frames.push(trimmed);
|
|
126
|
+
}
|
|
127
|
+
function reportOversizedFrames(output, count) {
|
|
128
|
+
for (let index = 0; index < count; index += 1) {
|
|
129
|
+
output.write("aw mcp serve-db: frame JSON-RPC demasiado grande descartado\n");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function enqueueConsumption(consumed, deps, enqueue) {
|
|
133
|
+
reportOversizedFrames(deps.diagnostics, consumed.oversizedFrames);
|
|
134
|
+
for (const frame of consumed.frames)
|
|
135
|
+
enqueue(frame);
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=database-mcp-stdio.js.map
|