engineering-memory 1.6.0 → 1.6.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -312,13 +312,19 @@ export class BridgeService {
|
|
|
312
312
|
resumeIdentityMatches &&
|
|
313
313
|
conflicts.every(isOfflineDevelopmentWarning) &&
|
|
314
314
|
isOfflineLeaseUsable(resumedLease, sessionId, pointer?.changeBaseline?.leasePaths ?? [], pointer?.changeBaseline?.diffHash);
|
|
315
|
-
const
|
|
316
|
-
pointer.sessionId === sessionId &&
|
|
317
|
-
pointer.changeBaseline &&
|
|
318
|
-
(resumedLease?.baselineDiffHash === pointer.changeBaseline.diffHash ||
|
|
319
|
-
objectValue(backend.verificationRecovery)?.diffHash === repository.git.diffHash)
|
|
315
|
+
const localBaseline = pointer?.taskId === backendTask.id && pointer.sessionId === sessionId
|
|
320
316
|
? pointer.changeBaseline
|
|
321
317
|
: undefined;
|
|
318
|
+
const recordedBaselineDiffHash = typeof backend.changeBaselineDiffHash === 'string'
|
|
319
|
+
? backend.changeBaselineDiffHash
|
|
320
|
+
: undefined;
|
|
321
|
+
const repairedBaseline = repairChangeBaseline(localBaseline, recordedBaselineDiffHash, repository.git.head);
|
|
322
|
+
const resumedBaseline = repairedBaseline ?? localBaseline;
|
|
323
|
+
if (recordedBaselineDiffHash &&
|
|
324
|
+
resumedBaseline &&
|
|
325
|
+
resumedBaseline.diffHash !== recordedBaselineDiffHash) {
|
|
326
|
+
conflicts.push('task_change_baseline_recaptured');
|
|
327
|
+
}
|
|
322
328
|
const resumedLastSequence = responseSource === 'stale_cache' && pointer
|
|
323
329
|
? Math.max(pointer.lastSequence, numericSequence(backendTask.lastSequence), localJournal.projection?.appliedEventIds.length ?? 0)
|
|
324
330
|
: numericSequence(backendTask.lastSequence);
|
|
@@ -384,7 +390,7 @@ export class BridgeService {
|
|
|
384
390
|
const pendingOutbox = await this.dependencies.outbox.list();
|
|
385
391
|
const pointer = await this.dependencies.activeContexts.load(repository.repoFingerprint);
|
|
386
392
|
if (pointer?.verificationIntent || pointer?.closeIntent) {
|
|
387
|
-
throw
|
|
393
|
+
throw refuse('A verification or close intent from an earlier session is still unsettled, so a new lease would be taken against the wrong state.', 'session.resume');
|
|
388
394
|
}
|
|
389
395
|
const conflicts = [
|
|
390
396
|
...(pointer?.resumeConflicts ?? []),
|
|
@@ -416,7 +422,7 @@ export class BridgeService {
|
|
|
416
422
|
let transitionTaskVersion;
|
|
417
423
|
if (input.transitionToWrite) {
|
|
418
424
|
if (!pointer || pointer.sessionId !== input.sessionId) {
|
|
419
|
-
throw
|
|
425
|
+
throw refuse('A read-only task can only become a write task from the session that owns it.', 'session.resume');
|
|
420
426
|
}
|
|
421
427
|
const refreshed = await this.activeTaskSnapshot(pointer.taskId, pointer.projectId, repository.repoRoot);
|
|
422
428
|
transitionTaskVersion = refreshed.taskVersion;
|
|
@@ -970,7 +976,7 @@ export class BridgeService {
|
|
|
970
976
|
const snapshotTask = objectValue(snapshot?.task);
|
|
971
977
|
const activeLease = objectValue(snapshot?.activeLease);
|
|
972
978
|
if (!snapshotTask || snapshotTask.id !== input.taskId) {
|
|
973
|
-
throw
|
|
979
|
+
throw refuse('The backend is holding a different task than the one being verified.', 'session.resume');
|
|
974
980
|
}
|
|
975
981
|
const recoveredVerification = await this.retryVerificationRecovery(repository, pointer, snapshot);
|
|
976
982
|
if (recoveredVerification) {
|
|
@@ -984,7 +990,7 @@ export class BridgeService {
|
|
|
984
990
|
const validations = normalizePersistentInput(input.validations, repository.repoRoot);
|
|
985
991
|
if (taskMode === 'read_only') {
|
|
986
992
|
if (input.leaseId || input.changedPaths?.length) {
|
|
987
|
-
throw
|
|
993
|
+
throw refuse('This task is read-only, so it has no lease and no changed paths to report.', 'task.verify');
|
|
988
994
|
}
|
|
989
995
|
}
|
|
990
996
|
else {
|
|
@@ -992,8 +998,11 @@ export class BridgeService {
|
|
|
992
998
|
const leaseChangedPaths = Array.isArray(activeLease?.changedPaths)
|
|
993
999
|
? activeLease.changedPaths.filter((path) => typeof path === 'string')
|
|
994
1000
|
: [];
|
|
995
|
-
if (!baseline
|
|
996
|
-
throw
|
|
1001
|
+
if (!baseline) {
|
|
1002
|
+
throw refuse('This task has no local change baseline, so what it changed cannot be measured. The baseline is restored from the task record.', 'session.resume');
|
|
1003
|
+
}
|
|
1004
|
+
if (activeLease?.baselineDiffHash !== baseline.diffHash) {
|
|
1005
|
+
throw refuse(`The active lease was taken against baseline ${short(activeLease?.baselineDiffHash)} but this task started from ${short(baseline.diffHash)}, so the lease must be taken again against the task's own baseline.`, 'context.prepare_change');
|
|
997
1006
|
}
|
|
998
1007
|
const wholeTreeDelta = manifestDelta(baseline.changedPaths, repository.git.changedPaths);
|
|
999
1008
|
const leased = new Set(normalizeChangedPaths([...baseline.leasePaths, ...leaseChangedPaths]));
|
|
@@ -1006,15 +1015,18 @@ export class BridgeService {
|
|
|
1006
1015
|
if (input.changedPaths &&
|
|
1007
1016
|
stableStringify(normalizeChangedPaths(input.changedPaths)) !==
|
|
1008
1017
|
stableStringify(taskChangedPaths)) {
|
|
1009
|
-
throw
|
|
1018
|
+
throw refuse(`Reported changed paths do not match the actual Git manifest. ${describePathDisagreement(normalizeChangedPaths(input.changedPaths), taskChangedPaths)} The manifest is the difference between the tree this task started from and the tree now; report exactly what it says. A manifest that looks short means the baseline has moved, and resuming repairs it.`, 'session.resume');
|
|
1019
|
+
}
|
|
1020
|
+
if (taskChangedPaths.length === 0) {
|
|
1021
|
+
throw refuse('The manifest shows this task changed nothing. Either the work is not in the working tree, or the baseline has moved to equal it.', 'session.resume');
|
|
1010
1022
|
}
|
|
1011
|
-
if (
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
!pathsContainAll(
|
|
1016
|
-
|
|
1017
|
-
throw
|
|
1023
|
+
if (!activeLease || !input.leaseId || activeLease.id !== input.leaseId) {
|
|
1024
|
+
throw refuse(`Verification needs the lease the backend currently holds. It has ${short(activeLease?.id)} and you sent ${short(input.leaseId)}.`, 'context.prepare_change');
|
|
1025
|
+
}
|
|
1026
|
+
const unleased = taskChangedPaths.filter((path) => !pathsContainAll(baseline.leasePaths, [path]) ||
|
|
1027
|
+
!pathsContainAll(leaseChangedPaths, [path]));
|
|
1028
|
+
if (unleased.length > 0) {
|
|
1029
|
+
throw refuse(`These changed paths are outside the current lease: ${unleased.slice(0, 5).join(', ')}${unleased.length > 5 ? `, and ${unleased.length - 5} more` : ''}. The lease must cover every path this task changed.`, 'context.prepare_change');
|
|
1018
1030
|
}
|
|
1019
1031
|
assertNewResourceEvidence(taskChanges.flatMap((entry) => newMemoryResourceCandidate(entry, resourceDiscoveryPolicy)), input.newResources ?? []);
|
|
1020
1032
|
}
|
|
@@ -1107,7 +1119,7 @@ export class BridgeService {
|
|
|
1107
1119
|
const snapshot = objectValue(snapshotResponse.data);
|
|
1108
1120
|
const task = objectValue(snapshot?.task);
|
|
1109
1121
|
if (!task || task.id !== input.taskId) {
|
|
1110
|
-
throw
|
|
1122
|
+
throw refuse('The backend is holding a different task than the one being closed.', 'session.resume');
|
|
1111
1123
|
}
|
|
1112
1124
|
const taskChanges = pointer.changeBaseline
|
|
1113
1125
|
? manifestDelta(pointer.changeBaseline.changedPaths, repository.git.changedPaths)
|
|
@@ -1136,7 +1148,7 @@ export class BridgeService {
|
|
|
1136
1148
|
});
|
|
1137
1149
|
const abandoned = objectValue(response.data);
|
|
1138
1150
|
if (!abandoned || abandoned.abandoned !== true) {
|
|
1139
|
-
throw
|
|
1151
|
+
throw refuse('The backend did not record the abandonment, so the task is still open.', 'session.resume');
|
|
1140
1152
|
}
|
|
1141
1153
|
const forgotten = await this.dependencies.activeContexts.forget(repository.repoFingerprint, input.taskId);
|
|
1142
1154
|
await this.dependencies.gate.invalidateTask(input.taskId);
|
|
@@ -2077,6 +2089,7 @@ export class BridgeService {
|
|
|
2077
2089
|
kind: 'bridge_error',
|
|
2078
2090
|
message: error instanceof Error ? error.message : 'Bridge operation failed',
|
|
2079
2091
|
retryable: false,
|
|
2092
|
+
...(error instanceof RefusalError ? { recovery: error.recovery } : {}),
|
|
2080
2093
|
},
|
|
2081
2094
|
};
|
|
2082
2095
|
}
|
|
@@ -2372,6 +2385,46 @@ function isOfflineLeaseUsable(lease, sessionId, changedPaths, baselineDiffHash)
|
|
|
2372
2385
|
return false;
|
|
2373
2386
|
}
|
|
2374
2387
|
}
|
|
2388
|
+
class RefusalError extends Error {
|
|
2389
|
+
recovery;
|
|
2390
|
+
constructor(message, recovery) {
|
|
2391
|
+
super(message);
|
|
2392
|
+
this.recovery = recovery;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
function refuse(message, recovery) {
|
|
2396
|
+
return new RefusalError(`${message} Recovery: call ${recovery}.`, recovery);
|
|
2397
|
+
}
|
|
2398
|
+
function short(value) {
|
|
2399
|
+
return typeof value === 'string' && value.length > 0 ? value.slice(0, 12) : 'none';
|
|
2400
|
+
}
|
|
2401
|
+
function describePathDisagreement(reported, manifest) {
|
|
2402
|
+
const inManifest = new Set(manifest);
|
|
2403
|
+
const inReport = new Set(reported);
|
|
2404
|
+
const missing = manifest.filter((path) => !inReport.has(path));
|
|
2405
|
+
const extra = reported.filter((path) => !inManifest.has(path));
|
|
2406
|
+
const sample = (paths) => paths.slice(0, 5).join(', ') + (paths.length > 5 ? `, and ${paths.length - 5} more` : '');
|
|
2407
|
+
const parts = [`You reported ${reported.length}; the manifest has ${manifest.length}.`];
|
|
2408
|
+
if (missing.length > 0)
|
|
2409
|
+
parts.push(`Missing from your report: ${sample(missing)}.`);
|
|
2410
|
+
if (extra.length > 0)
|
|
2411
|
+
parts.push(`Reported but not in the manifest: ${sample(extra)}.`);
|
|
2412
|
+
return parts.join(' ');
|
|
2413
|
+
}
|
|
2414
|
+
function repairChangeBaseline(local, recordedDiffHash, head) {
|
|
2415
|
+
if (!recordedDiffHash || local?.diffHash === recordedDiffHash)
|
|
2416
|
+
return undefined;
|
|
2417
|
+
if (sha256(`${head ?? '<unborn>'}
|
|
2418
|
+
${stableStringify([])}
|
|
2419
|
+
`) !== recordedDiffHash) {
|
|
2420
|
+
return undefined;
|
|
2421
|
+
}
|
|
2422
|
+
return {
|
|
2423
|
+
diffHash: recordedDiffHash,
|
|
2424
|
+
changedPaths: [],
|
|
2425
|
+
leasePaths: local?.leasePaths ?? [],
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2375
2428
|
function isOfflineDevelopmentWarning(value) {
|
|
2376
2429
|
return (value === 'backend_resume_is_stale' ||
|
|
2377
2430
|
value === 'pending_outbox_delivery' ||
|
|
@@ -95,6 +95,22 @@ Where the same product has a client and a backend, they are two projects. Adopt
|
|
|
95
95
|
then link them with `project.link` once both exist, and ask before linking rather than inferring it
|
|
96
96
|
from the names.
|
|
97
97
|
|
|
98
|
+
## The Task Baseline
|
|
99
|
+
|
|
100
|
+
A write task's baseline is the tree it started from, and every changed path it reports is the
|
|
101
|
+
difference between that tree and the current one. It is captured once, at the first change
|
|
102
|
+
preparation, and it is never re-captured — a baseline taken again halfway through a task equals the
|
|
103
|
+
finished work, and from then on the task reports the two or three files touched since instead of
|
|
104
|
+
everything it did. Verification then refuses the true path list, and reporting the short one would
|
|
105
|
+
record a task that did something it did not do.
|
|
106
|
+
|
|
107
|
+
`session.resume` therefore carries the baseline through a lease going stale, expiring, or being
|
|
108
|
+
replaced, and repairs one that has already drifted when the backend's record of the original can be
|
|
109
|
+
proved against the current commit. When it cannot prove it, resume reports
|
|
110
|
+
`task_change_baseline_recaptured`. Do not work around that conflict by reporting a shorter path
|
|
111
|
+
list, and do not edit the pointer file: say what happened, and let the user decide between
|
|
112
|
+
reopening the task and abandoning it.
|
|
113
|
+
|
|
98
114
|
## Change Preparation
|
|
99
115
|
|
|
100
116
|
Skip change preparation for a read-only task. Do not request an edit lease, send changed paths, record `pre_edit`, or reconcile code resources in that mode. If the user later authorizes a change, call `context.prepare_change` with `transitionToWrite: true`, the bridge-owned current task version, intended paths, and current baseline diff. Continue only after the bridge returns the updated write task and lease.
|