engineering-memory 1.11.7 → 1.11.9
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/dispatcher/sections.mjs +1 -1
- package/install/files.mjs +114 -8
- package/install/installer.mjs +20 -4
- package/install/launcher.mjs +95 -0
- package/package.json +1 -1
- package/runtime/build.json +3 -0
- package/runtime/dist/src/config.js +3 -0
- package/runtime/dist/src/index.js +25 -81
- package/runtime/dist/src/mcp/server.js +14 -7
- package/runtime/dist/src/mcp/tool-host.js +12 -0
- package/runtime/dist/src/runtime/bridge-service.js +80 -20
- package/runtime/dist/src/runtime/create-bridge-service.js +82 -0
- package/runtime/dist/src/runtime/runtime-entry.js +16 -0
- package/runtime/dist/src/runtime/runtime-host.js +265 -0
- package/runtime/dist/src/runtime/worktree-pool.js +145 -32
- package/runtime/dist/src/utilities/files.js +4 -1
- package/skill/references/lifecycle.md +1 -1
|
@@ -399,9 +399,15 @@ export class BridgeService {
|
|
|
399
399
|
(repository.projectId && repository.projectId !== projectId)) {
|
|
400
400
|
throw refuse('The live task belongs to a different project than the one this repository is bound to.', 'project.resolve');
|
|
401
401
|
}
|
|
402
|
-
|
|
402
|
+
const adoptable = pointer &&
|
|
403
|
+
!pointer.worktreeGeneration &&
|
|
404
|
+
normalizeTaskMode(pointer.mode) !== 'read_only' &&
|
|
405
|
+
this.dependencies.worktreePool
|
|
406
|
+
? await this.dependencies.worktreePool.find(projectId, taskSlug, repository.repoRoot)
|
|
407
|
+
: null;
|
|
408
|
+
if (pointer && this.dependencies.worktreePool && (pointer.worktreeGeneration || adoptable)) {
|
|
403
409
|
const pool = this.dependencies.worktreePool;
|
|
404
|
-
const previous = await pool.find(projectId, taskSlug, repository.repoRoot);
|
|
410
|
+
const previous = adoptable ?? (await pool.find(projectId, taskSlug, repository.repoRoot));
|
|
405
411
|
if (previous && previous.repoRoot !== repository.repoRoot)
|
|
406
412
|
return asJsonValue({
|
|
407
413
|
worktreeResumeRequired: true,
|
|
@@ -411,8 +417,11 @@ export class BridgeService {
|
|
|
411
417
|
nextAction: 'Call session.resume from this returned repoRoot and use it for every subsequent command.',
|
|
412
418
|
});
|
|
413
419
|
const sourceCommit = previous?.baseCommit ?? pointer.source?.sourceCommit;
|
|
420
|
+
const branch = pointer.worktreeGeneration
|
|
421
|
+
? pointer.branch
|
|
422
|
+
: (previous?.branch ?? pointer.branch);
|
|
414
423
|
if ((!sourceCommit && !(previous && previous.baseCommit === null && !previous.managed)) ||
|
|
415
|
-
|
|
424
|
+
branch === undefined)
|
|
416
425
|
throw refuse('The task branch and base must be restored before resuming its worktree.', 'worktree.reconcile');
|
|
417
426
|
const allocation = await pool.allocate({
|
|
418
427
|
projectId,
|
|
@@ -420,7 +429,7 @@ export class BridgeService {
|
|
|
420
429
|
repoFingerprint: repository.repoFingerprint,
|
|
421
430
|
repoRoot: repository.repoRoot,
|
|
422
431
|
externalTaskId: taskSlug,
|
|
423
|
-
branch
|
|
432
|
+
branch,
|
|
424
433
|
baseCommit: sourceCommit ?? null,
|
|
425
434
|
keepCurrent: previous ? !previous.managed : false,
|
|
426
435
|
resume: true,
|
|
@@ -433,6 +442,7 @@ export class BridgeService {
|
|
|
433
442
|
...pointer,
|
|
434
443
|
worktreeId: sha256(allocation.repoRoot),
|
|
435
444
|
worktreeGeneration: allocation.generation,
|
|
445
|
+
...(branch !== undefined ? { branch } : {}),
|
|
436
446
|
};
|
|
437
447
|
await this.dependencies.activeContexts.save(pointer);
|
|
438
448
|
if (allocation.repoRoot !== repository.repoRoot)
|
|
@@ -449,7 +459,7 @@ export class BridgeService {
|
|
|
449
459
|
const recoveredClose = await this.retryCloseIntent(repository, pointer);
|
|
450
460
|
return asJsonValue({
|
|
451
461
|
backend: { close: recoveredClose },
|
|
452
|
-
worktree: this.
|
|
462
|
+
worktree: (await this.ownedAllocation(projectId, repository.repoRoot)) ?? null,
|
|
453
463
|
pendingQuestionnaires,
|
|
454
464
|
repository: publicRepository(repository),
|
|
455
465
|
localJournal: await this.dependencies.journal.load(projectId, taskSlug),
|
|
@@ -632,7 +642,7 @@ export class BridgeService {
|
|
|
632
642
|
return asJsonValue({
|
|
633
643
|
backend,
|
|
634
644
|
repository: publicRepository(repository),
|
|
635
|
-
worktree: this.
|
|
645
|
+
worktree: (await this.ownedAllocation(projectId, repository.repoRoot)) ?? null,
|
|
636
646
|
localJournal,
|
|
637
647
|
pendingQuestionnaires,
|
|
638
648
|
synchronization: {
|
|
@@ -731,7 +741,7 @@ export class BridgeService {
|
|
|
731
741
|
transitionTaskVersion = refreshed.taskVersion;
|
|
732
742
|
}
|
|
733
743
|
if (decision && this.dependencies.worktreePool) {
|
|
734
|
-
const allocation = this.
|
|
744
|
+
const allocation = await this.ownedAllocation(pointer.projectId, repository.repoRoot);
|
|
735
745
|
if (!allocation || allocation.externalTaskId !== pointer.taskSlug)
|
|
736
746
|
throw refuse('Choose this task branch and use its allocated repoRoot before transitioning to write.', 'task.branch');
|
|
737
747
|
await this.dependencies.worktreePool.assertOwnership(pointer.projectId, repository.repoRoot, allocation.generation);
|
|
@@ -792,16 +802,14 @@ export class BridgeService {
|
|
|
792
802
|
await this.dependencies.repositories.git
|
|
793
803
|
.branchStore(repository.repoRoot)
|
|
794
804
|
.bind(preparedPointer.projectId, preparedPointer.taskSlug, preparedPointer.taskId);
|
|
805
|
+
const allocation = await this.ownedAllocation(preparedPointer.projectId, repository.repoRoot);
|
|
795
806
|
await this.dependencies.activeContexts.save({
|
|
796
807
|
...preparedPointer,
|
|
797
808
|
worktreeId: sha256(repository.repoRoot),
|
|
798
809
|
branch: typeof preparedTask?.branch === 'string' ? preparedTask.branch : decision.branch,
|
|
799
810
|
mode: 'write',
|
|
800
|
-
...(
|
|
801
|
-
? { worktreeGeneration: this.poolAllocations.get(repository.repoRoot).generation }
|
|
802
|
-
: {}),
|
|
811
|
+
...(allocation ? { worktreeGeneration: allocation.generation } : {}),
|
|
803
812
|
});
|
|
804
|
-
const allocation = this.poolAllocations.get(repository.repoRoot);
|
|
805
813
|
if (allocation && this.dependencies.worktreePool) {
|
|
806
814
|
await this.dependencies.worktreePool.bind(preparedPointer.projectId, repository.repoRoot, allocation.generation, preparedPointer.taskId);
|
|
807
815
|
allocation.taskId = preparedPointer.taskId;
|
|
@@ -1884,11 +1892,26 @@ export class BridgeService {
|
|
|
1884
1892
|
const pool = this.dependencies.worktreePool;
|
|
1885
1893
|
if (!projectId || !pool)
|
|
1886
1894
|
throw refuse('Resolve the project with a pool-capable client.', 'project.resolve');
|
|
1887
|
-
if (operation === 'list' || operation === 'reconcile')
|
|
1895
|
+
if (operation === 'list' || operation === 'reconcile') {
|
|
1896
|
+
const managedRoot = await pool.managedRoot();
|
|
1888
1897
|
return asJsonValue({
|
|
1889
1898
|
items: await pool.reconcile(projectId, repository.repoRoot, await this.poolPolicy()),
|
|
1890
1899
|
selected: await pool.forPath(projectId, repository.repoRoot),
|
|
1900
|
+
managedRoot,
|
|
1901
|
+
...(managedRoot.problem || managedRoot.synced
|
|
1902
|
+
? {
|
|
1903
|
+
notice: [
|
|
1904
|
+
...(managedRoot.problem ? [managedRoot.problem] : []),
|
|
1905
|
+
...(managedRoot.synced
|
|
1906
|
+
? [
|
|
1907
|
+
`Managed worktrees are created under ${managedRoot.root}, which a cloud sync client watches, so every Git operation there is re-scanned and uploaded. To create new worktrees elsewhere, write {"root": "<absolute directory>"} to ${managedRoot.override}; it takes effect for the next allocation, and existing worktrees keep working where they are.`,
|
|
1908
|
+
]
|
|
1909
|
+
: []),
|
|
1910
|
+
].join(' '),
|
|
1911
|
+
}
|
|
1912
|
+
: {}),
|
|
1891
1913
|
});
|
|
1914
|
+
}
|
|
1892
1915
|
if (operation === 'release_unowned') {
|
|
1893
1916
|
const entry = await pool.forPath(projectId, repository.repoRoot);
|
|
1894
1917
|
if (!entry || !input.sourceCommit || !input.generation || !input.decisionId)
|
|
@@ -1974,12 +1997,46 @@ export class BridgeService {
|
|
|
1974
1997
|
resume: Boolean(existing),
|
|
1975
1998
|
}, await this.poolPolicy(true));
|
|
1976
1999
|
this.poolAllocations.set(allocation.repoRoot, allocation);
|
|
2000
|
+
if (existing && allocation.repoRoot === repository.repoRoot) {
|
|
2001
|
+
const current = await this.dependencies.activeContexts.loadForSlug(repository.repoFingerprint, input.externalTaskId);
|
|
2002
|
+
if (current &&
|
|
2003
|
+
current.taskSlug === input.externalTaskId &&
|
|
2004
|
+
!current.worktreeGeneration &&
|
|
2005
|
+
normalizeTaskMode(current.mode) !== 'read_only')
|
|
2006
|
+
await this.dependencies.activeContexts.save({
|
|
2007
|
+
...current,
|
|
2008
|
+
worktreeId: sha256(allocation.repoRoot),
|
|
2009
|
+
worktreeGeneration: allocation.generation,
|
|
2010
|
+
...(current.branch === undefined && allocation.branch !== null
|
|
2011
|
+
? { branch: allocation.branch }
|
|
2012
|
+
: {}),
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
1977
2015
|
return asJsonValue({
|
|
1978
2016
|
...allocation,
|
|
1979
2017
|
kept: !allocation.managed,
|
|
1980
2018
|
nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot. Use task.heartbeat during long work. Pause before handing the task to another client.',
|
|
1981
2019
|
});
|
|
1982
2020
|
}
|
|
2021
|
+
adoptOwned(repoRoot, entry) {
|
|
2022
|
+
const cached = this.poolAllocations.get(repoRoot);
|
|
2023
|
+
if (cached)
|
|
2024
|
+
return cached;
|
|
2025
|
+
const pool = this.dependencies.worktreePool;
|
|
2026
|
+
if (!entry || !pool || !pool.owns(entry))
|
|
2027
|
+
return undefined;
|
|
2028
|
+
this.poolAllocations.set(repoRoot, entry);
|
|
2029
|
+
return entry;
|
|
2030
|
+
}
|
|
2031
|
+
async ownedAllocation(projectId, repoRoot) {
|
|
2032
|
+
const cached = this.poolAllocations.get(repoRoot);
|
|
2033
|
+
if (cached)
|
|
2034
|
+
return cached;
|
|
2035
|
+
const pool = this.dependencies.worktreePool;
|
|
2036
|
+
if (!pool)
|
|
2037
|
+
return undefined;
|
|
2038
|
+
return this.adoptOwned(repoRoot, await pool.forPath(projectId, repoRoot));
|
|
2039
|
+
}
|
|
1983
2040
|
async assertPoolPointer(repository, pointer) {
|
|
1984
2041
|
const pool = this.dependencies.worktreePool;
|
|
1985
2042
|
if (!pool)
|
|
@@ -1987,11 +2044,10 @@ export class BridgeService {
|
|
|
1987
2044
|
const entry = await pool.forPath(pointer.projectId, repository.repoRoot);
|
|
1988
2045
|
if (!entry && !pointer.worktreeGeneration)
|
|
1989
2046
|
return; // existing unmanaged tasks retain their legacy reservation
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
entry.externalTaskId !== pointer.taskSlug)
|
|
2047
|
+
if (entry && entry.externalTaskId !== pointer.taskSlug)
|
|
2048
|
+
throw refuse(`This directory is allocated to task ${entry.externalTaskId}. Resume this task from its own worktree, or reconcile the pool to release the directory.`, 'worktree.reconcile');
|
|
2049
|
+
const owned = this.adoptOwned(repository.repoRoot, entry);
|
|
2050
|
+
if (!entry || !owned || owned.generation !== pointer.worktreeGeneration)
|
|
1995
2051
|
throw refuse('Resume this task to acquire its worktree before writing or delivering.', 'session.resume');
|
|
1996
2052
|
await pool.assertOwnership(pointer.projectId, repository.repoRoot, owned.generation);
|
|
1997
2053
|
}
|
|
@@ -2117,7 +2173,9 @@ export class BridgeService {
|
|
|
2117
2173
|
if (!abandoned || abandoned.abandoned !== true) {
|
|
2118
2174
|
throw refuse('The backend did not record the abandonment, so the task is still open.', 'session.resume');
|
|
2119
2175
|
}
|
|
2120
|
-
const allocation =
|
|
2176
|
+
const allocation = repository.projectId
|
|
2177
|
+
? await this.ownedAllocation(repository.projectId, repository.repoRoot)
|
|
2178
|
+
: this.poolAllocations.get(repository.repoRoot);
|
|
2121
2179
|
if (allocation && this.dependencies.worktreePool) {
|
|
2122
2180
|
await this.dependencies.worktreePool.pause(allocation.projectId, allocation.repoRoot, allocation.generation);
|
|
2123
2181
|
this.poolAllocations.delete(repository.repoRoot);
|
|
@@ -3385,7 +3443,9 @@ export class BridgeService {
|
|
|
3385
3443
|
taskChanges,
|
|
3386
3444
|
});
|
|
3387
3445
|
await this.dependencies.activeContexts.clearCloseIntent(repository.repoFingerprint, String(body.taskId));
|
|
3388
|
-
const allocation =
|
|
3446
|
+
const allocation = repository.projectId
|
|
3447
|
+
? await this.ownedAllocation(repository.projectId, repository.repoRoot)
|
|
3448
|
+
: this.poolAllocations.get(repository.repoRoot);
|
|
3389
3449
|
if (allocation && this.dependencies.worktreePool)
|
|
3390
3450
|
await this.dependencies.worktreePool.delivery(allocation.projectId, repository.repoRoot, allocation.generation);
|
|
3391
3451
|
else
|
|
@@ -3471,7 +3531,7 @@ export class BridgeService {
|
|
|
3471
3531
|
try {
|
|
3472
3532
|
const allocation = await this.dependencies.worktreePool?.forTask(taskId);
|
|
3473
3533
|
if (allocation) {
|
|
3474
|
-
const owned = this.
|
|
3534
|
+
const owned = this.adoptOwned(allocation.repoRoot, allocation);
|
|
3475
3535
|
if (!owned || owned.generation !== allocation.generation)
|
|
3476
3536
|
throw refuse('Resume the task in its owned worktree before this operation.', 'session.resume');
|
|
3477
3537
|
await this.dependencies.worktreePool.assertOwnership(allocation.projectId, allocation.repoRoot, owned.generation);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { WorktreePool } from './worktree-pool.js';
|
|
2
|
+
import { WorktreePolicyCache } from './worktree-policy.js';
|
|
3
|
+
import { BrowserAuthCoordinator } from '../auth/browser-auth.js';
|
|
4
|
+
import { createCredentialStore } from '../auth/credential-store.js';
|
|
5
|
+
import { EtagCache } from '../cache/etag-cache.js';
|
|
6
|
+
import { apiNamespaceKey, apiStateRoot, endpoints, loadBridgeConfig } from '../config.js';
|
|
7
|
+
import { GitInspector } from '../git/git-inspector.js';
|
|
8
|
+
import { VerificationGate } from '../git/verification-gate.js';
|
|
9
|
+
import { JournalStore } from '../journal/journal-store.js';
|
|
10
|
+
import { RepositoryResolver } from '../project/repository.js';
|
|
11
|
+
import { ApiClient } from './api-client.js';
|
|
12
|
+
import { BridgeService } from './bridge-service.js';
|
|
13
|
+
import { OfflineOutbox } from './offline-outbox.js';
|
|
14
|
+
import { ActiveContextStore } from './active-context-store.js';
|
|
15
|
+
import { RepositoryDecisionStore } from './repository-decision-store.js';
|
|
16
|
+
import { ShadowNoticeStore } from './shadow-notice-store.js';
|
|
17
|
+
import { UpdateChoiceStore } from './update-choice-store.js';
|
|
18
|
+
import { PrincipalStateGuard } from './principal-state.js';
|
|
19
|
+
import { OnboardingStore } from './onboarding-store.js';
|
|
20
|
+
import { QuestionnaireStore } from './questionnaire-store.js';
|
|
21
|
+
export function createBridgeService(options = {}) {
|
|
22
|
+
const config = loadBridgeConfig();
|
|
23
|
+
const apiNamespace = apiNamespaceKey(config.apiBaseUrl);
|
|
24
|
+
const stateRoot = apiStateRoot(config);
|
|
25
|
+
const credentials = createCredentialStore({
|
|
26
|
+
service: config.credentialService,
|
|
27
|
+
account: `${config.credentialAccount}:${apiNamespace}`,
|
|
28
|
+
stateRoot,
|
|
29
|
+
});
|
|
30
|
+
const cache = new EtagCache({
|
|
31
|
+
stateRoot,
|
|
32
|
+
maxEntries: config.cacheMaxEntries,
|
|
33
|
+
maxBytes: config.cacheMaxBytes,
|
|
34
|
+
maxAgeMs: config.cacheMaxAgeMs,
|
|
35
|
+
});
|
|
36
|
+
const client = new ApiClient({
|
|
37
|
+
baseUrl: config.apiBaseUrl,
|
|
38
|
+
timeoutMs: config.requestTimeoutMs,
|
|
39
|
+
credentials,
|
|
40
|
+
cache,
|
|
41
|
+
refreshPath: endpoints.authRefresh,
|
|
42
|
+
});
|
|
43
|
+
const git = new GitInspector();
|
|
44
|
+
const outbox = new OfflineOutbox(stateRoot);
|
|
45
|
+
const activeContexts = new ActiveContextStore(stateRoot);
|
|
46
|
+
const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
|
|
47
|
+
const updateChoices = new UpdateChoiceStore(stateRoot);
|
|
48
|
+
const shadowNotices = new ShadowNoticeStore(stateRoot);
|
|
49
|
+
const worktreePool = new WorktreePool(stateRoot, {
|
|
50
|
+
...(options.ownerId ? { ownerId: options.ownerId } : {}),
|
|
51
|
+
pendingWork: async (entry) => {
|
|
52
|
+
if (!entry.taskId)
|
|
53
|
+
return false;
|
|
54
|
+
const pointer = await activeContexts.loadForTask(entry.repoFingerprint, entry.taskId);
|
|
55
|
+
return Boolean(pointer?.verificationIntent ||
|
|
56
|
+
pointer?.closeIntent ||
|
|
57
|
+
(await outbox.listForTask(entry.taskId)).length);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const gate = new VerificationGate(stateRoot, git, outbox, undefined, worktreePool);
|
|
61
|
+
const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
|
|
62
|
+
return new BridgeService({
|
|
63
|
+
worktreePool,
|
|
64
|
+
worktreePolicy: new WorktreePolicyCache(stateRoot),
|
|
65
|
+
client,
|
|
66
|
+
credentials,
|
|
67
|
+
browserAuth: new BrowserAuthCoordinator(client, credentials),
|
|
68
|
+
repositories: new RepositoryResolver(git, config.markerSchemaVersion, stateRoot),
|
|
69
|
+
journal: new JournalStore(stateRoot),
|
|
70
|
+
outbox,
|
|
71
|
+
gate,
|
|
72
|
+
activeContexts,
|
|
73
|
+
repositoryDecisions,
|
|
74
|
+
updateChoices,
|
|
75
|
+
shadowNotices,
|
|
76
|
+
questionnaires: new QuestionnaireStore(stateRoot),
|
|
77
|
+
onboarding: new OnboardingStore(stateRoot),
|
|
78
|
+
clientVersion: options.clientVersion === undefined ? config.clientVersion : options.clientVersion,
|
|
79
|
+
principalState,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=create-bridge-service.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { registerAllTools } from '../mcp/server.js';
|
|
2
|
+
import { recordingHost } from '../mcp/tool-host.js';
|
|
3
|
+
import { createBridgeService } from './create-bridge-service.js';
|
|
4
|
+
export function collectTools(service, server) {
|
|
5
|
+
const recording = recordingHost(server);
|
|
6
|
+
registerAllTools(recording.host, service);
|
|
7
|
+
return recording.tools;
|
|
8
|
+
}
|
|
9
|
+
export function createRuntime(options) {
|
|
10
|
+
const service = createBridgeService({
|
|
11
|
+
ownerId: options.ownerId,
|
|
12
|
+
clientVersion: options.clientVersion,
|
|
13
|
+
});
|
|
14
|
+
return { tools: collectTools(service, options.server) };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=runtime-entry.js.map
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export const currentReleaseSchema = z.object({
|
|
7
|
+
product: z.literal('engineering-memory'),
|
|
8
|
+
schemaVersion: z.literal(1),
|
|
9
|
+
path: z.string().min(1),
|
|
10
|
+
version: z.string().min(1).nullable().optional(),
|
|
11
|
+
sourceHash: z.string().optional(),
|
|
12
|
+
bridgeEntry: z.string().optional(),
|
|
13
|
+
gateEntry: z.string().optional(),
|
|
14
|
+
});
|
|
15
|
+
export const runtimeEntryRelativePath = ['dist', 'src', 'runtime', 'runtime-entry.js'];
|
|
16
|
+
const emptyObjectSchema = { type: 'object', properties: {} };
|
|
17
|
+
export async function readCurrentRelease(currentPath) {
|
|
18
|
+
let raw;
|
|
19
|
+
try {
|
|
20
|
+
raw = await readFile(currentPath, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return { problem: `${currentPath} cannot be read: ${describe(error)}` };
|
|
24
|
+
}
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { problem: `${currentPath} is not valid JSON` };
|
|
31
|
+
}
|
|
32
|
+
const release = currentReleaseSchema.safeParse(parsed);
|
|
33
|
+
if (!release.success)
|
|
34
|
+
return { problem: `${currentPath} does not describe a release` };
|
|
35
|
+
const path = isAbsolute(release.data.path)
|
|
36
|
+
? resolve(release.data.path)
|
|
37
|
+
: resolve(currentPath, '..', release.data.path);
|
|
38
|
+
return { release: release.data, path };
|
|
39
|
+
}
|
|
40
|
+
export async function importRelease(releasePath) {
|
|
41
|
+
const entry = join(releasePath, ...runtimeEntryRelativePath);
|
|
42
|
+
const loaded = (await import(pathToFileURL(entry).href));
|
|
43
|
+
if (typeof loaded.createRuntime !== 'function')
|
|
44
|
+
throw new Error(`${entry} does not export createRuntime`);
|
|
45
|
+
return { createRuntime: loaded.createRuntime };
|
|
46
|
+
}
|
|
47
|
+
export class RuntimeHost {
|
|
48
|
+
options;
|
|
49
|
+
ownerId;
|
|
50
|
+
registered = new Map();
|
|
51
|
+
specs = new Map();
|
|
52
|
+
loadedPath;
|
|
53
|
+
loadedVersion;
|
|
54
|
+
refreshing;
|
|
55
|
+
failedPath;
|
|
56
|
+
seen;
|
|
57
|
+
timer;
|
|
58
|
+
started = false;
|
|
59
|
+
constructor(options) {
|
|
60
|
+
this.options = options;
|
|
61
|
+
this.ownerId = options.ownerId ?? randomUUID();
|
|
62
|
+
this.loadedPath = resolve(options.release.path);
|
|
63
|
+
this.loadedVersion = options.release.version;
|
|
64
|
+
}
|
|
65
|
+
get version() {
|
|
66
|
+
return this.loadedVersion;
|
|
67
|
+
}
|
|
68
|
+
get releasePath() {
|
|
69
|
+
return this.loadedPath;
|
|
70
|
+
}
|
|
71
|
+
start() {
|
|
72
|
+
if (this.started)
|
|
73
|
+
return;
|
|
74
|
+
this.started = true;
|
|
75
|
+
this.apply(this.instantiate(this.options.release.module, this.options.release.version));
|
|
76
|
+
}
|
|
77
|
+
watch(intervalMs) {
|
|
78
|
+
if (this.timer || !this.options.currentPath)
|
|
79
|
+
return;
|
|
80
|
+
this.timer = setInterval(() => {
|
|
81
|
+
void this.refresh();
|
|
82
|
+
}, intervalMs);
|
|
83
|
+
this.timer.unref();
|
|
84
|
+
}
|
|
85
|
+
stop() {
|
|
86
|
+
if (this.timer)
|
|
87
|
+
clearInterval(this.timer);
|
|
88
|
+
this.timer = undefined;
|
|
89
|
+
}
|
|
90
|
+
async refresh() {
|
|
91
|
+
if (!this.options.currentPath || !this.started)
|
|
92
|
+
return false;
|
|
93
|
+
this.refreshing ??= this.reload().finally(() => {
|
|
94
|
+
this.refreshing = undefined;
|
|
95
|
+
});
|
|
96
|
+
return this.refreshing;
|
|
97
|
+
}
|
|
98
|
+
async reload() {
|
|
99
|
+
const currentPath = this.options.currentPath;
|
|
100
|
+
let marker;
|
|
101
|
+
try {
|
|
102
|
+
marker = await stat(currentPath);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (this.seen && this.seen.mtimeMs === marker.mtimeMs && this.seen.size === marker.size)
|
|
108
|
+
return false;
|
|
109
|
+
this.seen = { mtimeMs: marker.mtimeMs, size: marker.size };
|
|
110
|
+
const current = await readCurrentRelease(currentPath);
|
|
111
|
+
if ('problem' in current) {
|
|
112
|
+
this.log(`Engineering Memory bridge keeps release ${this.describeLoaded()}: ${current.problem}`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (current.path === this.loadedPath || current.path === this.failedPath)
|
|
116
|
+
return false;
|
|
117
|
+
const version = current.release.version ?? null;
|
|
118
|
+
try {
|
|
119
|
+
const module = await (this.options.load ?? importRelease)(current.path);
|
|
120
|
+
this.apply(this.instantiate(module, version));
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
this.failedPath = current.path;
|
|
124
|
+
this.log(`Engineering Memory bridge keeps release ${this.describeLoaded()} because ${current.path} could not be loaded: ${describe(error)}`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
this.loadedPath = current.path;
|
|
128
|
+
this.loadedVersion = version;
|
|
129
|
+
this.failedPath = undefined;
|
|
130
|
+
this.log(`Engineering Memory bridge switched to release ${this.describeLoaded()}`);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
instantiate(module, version) {
|
|
134
|
+
const runtime = module.createRuntime({
|
|
135
|
+
ownerId: this.ownerId,
|
|
136
|
+
clientVersion: version,
|
|
137
|
+
server: this.options.server.server,
|
|
138
|
+
});
|
|
139
|
+
if (!runtime || !Array.isArray(runtime.tools))
|
|
140
|
+
throw new Error('createRuntime did not return a tool list');
|
|
141
|
+
const names = new Set();
|
|
142
|
+
for (const spec of runtime.tools) {
|
|
143
|
+
if (typeof spec.name !== 'string' || !spec.name)
|
|
144
|
+
throw new Error('createRuntime returned a tool without a name');
|
|
145
|
+
if (names.has(spec.name))
|
|
146
|
+
throw new Error(`Tool ${spec.name} is defined twice`);
|
|
147
|
+
if (typeof spec.callback !== 'function')
|
|
148
|
+
throw new Error(`Tool ${spec.name} has no callback`);
|
|
149
|
+
names.add(spec.name);
|
|
150
|
+
}
|
|
151
|
+
return runtime;
|
|
152
|
+
}
|
|
153
|
+
apply(runtime) {
|
|
154
|
+
const names = new Set();
|
|
155
|
+
const register = this.options.server.registerTool.bind(this.options.server);
|
|
156
|
+
for (const spec of runtime.tools) {
|
|
157
|
+
names.add(spec.name);
|
|
158
|
+
this.specs.set(spec.name, spec);
|
|
159
|
+
const signature = toolSignature(spec.config);
|
|
160
|
+
const existing = this.registered.get(spec.name);
|
|
161
|
+
if (!existing) {
|
|
162
|
+
const tool = register(spec.name, { ...spec.config, inputSchema: this.inputSchemaOf(spec.name) }, this.dispatcher(spec.name));
|
|
163
|
+
this.registered.set(spec.name, { tool, signature });
|
|
164
|
+
}
|
|
165
|
+
else if (existing.signature !== signature) {
|
|
166
|
+
existing.tool.update({
|
|
167
|
+
...(spec.config.title !== undefined ? { title: spec.config.title } : {}),
|
|
168
|
+
...(spec.config.description !== undefined
|
|
169
|
+
? { description: spec.config.description }
|
|
170
|
+
: {}),
|
|
171
|
+
...(spec.config.outputSchema !== undefined
|
|
172
|
+
? { outputSchema: spec.config.outputSchema }
|
|
173
|
+
: {}),
|
|
174
|
+
...(spec.config.annotations !== undefined
|
|
175
|
+
? { annotations: spec.config.annotations }
|
|
176
|
+
: {}),
|
|
177
|
+
});
|
|
178
|
+
existing.signature = signature;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const [name, entry] of [...this.registered]) {
|
|
182
|
+
if (names.has(name))
|
|
183
|
+
continue;
|
|
184
|
+
entry.tool.remove();
|
|
185
|
+
this.registered.delete(name);
|
|
186
|
+
this.specs.delete(name);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
inputSchemaOf(name) {
|
|
190
|
+
const jsonSchema = (io) => (options) => {
|
|
191
|
+
const schema = this.specs.get(name)?.config.inputSchema;
|
|
192
|
+
if (!schema)
|
|
193
|
+
return emptyObjectSchema;
|
|
194
|
+
return schema['~standard'].jsonSchema[io](options);
|
|
195
|
+
};
|
|
196
|
+
const props = {
|
|
197
|
+
version: 1,
|
|
198
|
+
vendor: 'engineering-memory',
|
|
199
|
+
validate: (value) => ({ value }),
|
|
200
|
+
jsonSchema: { input: jsonSchema('input'), output: jsonSchema('output') },
|
|
201
|
+
};
|
|
202
|
+
return { '~standard': props };
|
|
203
|
+
}
|
|
204
|
+
dispatcher(name) {
|
|
205
|
+
return async (args, context) => {
|
|
206
|
+
await this.refresh();
|
|
207
|
+
const spec = this.specs.get(name);
|
|
208
|
+
if (!spec)
|
|
209
|
+
throw new Error(`Tool ${name} is no longer provided by this bridge release; list the tools again and use the current lifecycle.`);
|
|
210
|
+
const schema = spec.config.inputSchema;
|
|
211
|
+
if (!schema)
|
|
212
|
+
return spec.callback(context);
|
|
213
|
+
const outcome = await schema['~standard'].validate(args ?? {});
|
|
214
|
+
if (outcome.issues && outcome.issues.length > 0)
|
|
215
|
+
return {
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: 'text',
|
|
219
|
+
text: `Input validation error: Invalid arguments for tool ${name}: ${outcome.issues
|
|
220
|
+
.map((issue) => formatIssue(issue))
|
|
221
|
+
.join(', ')}`,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
isError: true,
|
|
225
|
+
};
|
|
226
|
+
return spec.callback(outcome.value, context);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
describeLoaded() {
|
|
230
|
+
return this.loadedVersion ?? this.loadedPath;
|
|
231
|
+
}
|
|
232
|
+
log(message) {
|
|
233
|
+
this.options.log?.(message);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export function toolSignature(config) {
|
|
237
|
+
return JSON.stringify({
|
|
238
|
+
title: config.title ?? null,
|
|
239
|
+
description: config.description ?? null,
|
|
240
|
+
annotations: config.annotations ?? null,
|
|
241
|
+
input: jsonSchemaOf(config.inputSchema),
|
|
242
|
+
output: jsonSchemaOf(config.outputSchema),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function jsonSchemaOf(schema) {
|
|
246
|
+
if (!schema)
|
|
247
|
+
return null;
|
|
248
|
+
try {
|
|
249
|
+
return schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' });
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return randomUUID();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function formatIssue(issue) {
|
|
256
|
+
if (!issue.path?.length)
|
|
257
|
+
return issue.message;
|
|
258
|
+
return `${issue.path
|
|
259
|
+
.map((segment) => String(typeof segment === 'object' && segment !== null ? segment.key : segment))
|
|
260
|
+
.join('.')}: ${issue.message}`;
|
|
261
|
+
}
|
|
262
|
+
function describe(error) {
|
|
263
|
+
return error instanceof Error ? error.message : String(error);
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=runtime-host.js.map
|