@yemi33/minions 0.1.2423 → 0.1.2425
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/bin/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +6 -4
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/internal-install.md +190 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +12 -7
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-auto-fix-dispatch.md +3 -3
- package/docs/pr-review-fix-loop.md +6 -6
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
package/engine/meeting.js
CHANGED
|
@@ -7,6 +7,7 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const shared = require('./shared');
|
|
9
9
|
const { safeJson, uid, log, ts, ENGINE_DEFAULTS, WORK_TYPE, DISPATCH_RESULT, MEETING_STATUS } = shared;
|
|
10
|
+
const apiValidation = require('./api-validation');
|
|
10
11
|
const queries = require('./queries');
|
|
11
12
|
const { getDispatch, getConfig } = queries;
|
|
12
13
|
const { renderPlaybook } = require('./playbook');
|
|
@@ -21,14 +22,31 @@ const EMPTY_OUTPUT_PATTERNS = ['(no output)', '(no findings)', '(no response)'];
|
|
|
21
22
|
// tests can redirect the meetings directory without patching module internals.
|
|
22
23
|
const MEETINGS_DIR = path.join(shared.MINIONS_DIR, 'meetings');
|
|
23
24
|
const MEETING_NOTE_ARTIFACT_ROOT = path.join(shared.MINIONS_DIR, 'notes', 'inbox');
|
|
24
|
-
const
|
|
25
|
+
const MEETING_LIMITS = Object.freeze({
|
|
26
|
+
maxTitleLength: 200,
|
|
27
|
+
maxAgendaLength: 20000,
|
|
28
|
+
maxNoteLength: 4000,
|
|
29
|
+
maxParticipants: 50,
|
|
30
|
+
maxIdentifierLength: 128,
|
|
31
|
+
});
|
|
32
|
+
const TERMINAL_MEETING_STATUSES = new Set([
|
|
33
|
+
MEETING_STATUS.COMPLETED,
|
|
34
|
+
MEETING_STATUS.ARCHIVED,
|
|
35
|
+
]);
|
|
25
36
|
const ROUND_STATUS_BY_NAME = {
|
|
26
|
-
investigate:
|
|
27
|
-
debate:
|
|
28
|
-
conclude:
|
|
37
|
+
investigate: MEETING_STATUS.INVESTIGATING,
|
|
38
|
+
debate: MEETING_STATUS.DEBATING,
|
|
39
|
+
conclude: MEETING_STATUS.CONCLUDING,
|
|
29
40
|
};
|
|
30
41
|
const ROUND_NUMBER_BY_NAME = { investigate: 1, debate: 2, conclude: 3 };
|
|
31
42
|
const ACTIVE_MEETING_STATUSES = new Set(Object.values(ROUND_STATUS_BY_NAME));
|
|
43
|
+
const MEETING_ACTION_STATUSES = Object.freeze({
|
|
44
|
+
note: ACTIVE_MEETING_STATUSES,
|
|
45
|
+
advance: ACTIVE_MEETING_STATUSES,
|
|
46
|
+
end: ACTIVE_MEETING_STATUSES,
|
|
47
|
+
archive: new Set([MEETING_STATUS.COMPLETED]),
|
|
48
|
+
unarchive: new Set([MEETING_STATUS.ARCHIVED]),
|
|
49
|
+
});
|
|
32
50
|
|
|
33
51
|
function isTerminalMeetingStatus(status) {
|
|
34
52
|
return TERMINAL_MEETING_STATUSES.has(String(status || '').toLowerCase());
|
|
@@ -50,6 +68,141 @@ function expectedMeetingStatusForRound(roundName) {
|
|
|
50
68
|
return ROUND_STATUS_BY_NAME[String(roundName || '').toLowerCase()] || null;
|
|
51
69
|
}
|
|
52
70
|
|
|
71
|
+
function validateMeetingId(value) {
|
|
72
|
+
return apiValidation.validateIdentifier(value, {
|
|
73
|
+
field: 'id',
|
|
74
|
+
path: 'id',
|
|
75
|
+
trim: true,
|
|
76
|
+
maxLength: MEETING_LIMITS.maxIdentifierLength,
|
|
77
|
+
pattern: /^MTG-[A-Za-z0-9_-]+$/,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validateMeetingCreateInput(input, config, options = {}) {
|
|
82
|
+
apiValidation.validatePlainObject(input, { field: 'body', path: 'body' });
|
|
83
|
+
const title = apiValidation.validateString(input.title, {
|
|
84
|
+
field: 'title',
|
|
85
|
+
path: 'title',
|
|
86
|
+
trim: true,
|
|
87
|
+
minLength: 1,
|
|
88
|
+
maxLength: MEETING_LIMITS.maxTitleLength,
|
|
89
|
+
});
|
|
90
|
+
const agenda = apiValidation.validateString(input.agenda, {
|
|
91
|
+
field: 'agenda',
|
|
92
|
+
path: 'agenda',
|
|
93
|
+
trim: true,
|
|
94
|
+
minLength: 1,
|
|
95
|
+
maxLength: MEETING_LIMITS.maxAgendaLength,
|
|
96
|
+
});
|
|
97
|
+
const minimumParticipants = Number.isInteger(options.minParticipants)
|
|
98
|
+
? options.minParticipants
|
|
99
|
+
: 0;
|
|
100
|
+
const sourceParticipants = input.participants === undefined ? [] : input.participants;
|
|
101
|
+
apiValidation.validateArray(sourceParticipants, {
|
|
102
|
+
field: 'participants',
|
|
103
|
+
path: 'participants',
|
|
104
|
+
minLength: minimumParticipants,
|
|
105
|
+
maxLength: MEETING_LIMITS.maxParticipants,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const participants = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
const configuredAgents = config && typeof config === 'object'
|
|
111
|
+
? Object.keys(config.agents || {})
|
|
112
|
+
: null;
|
|
113
|
+
sourceParticipants.forEach((participant, index) => {
|
|
114
|
+
const pathParts = ['participants', index];
|
|
115
|
+
const id = apiValidation.validateIdentifier(participant, {
|
|
116
|
+
field: 'participants',
|
|
117
|
+
path: pathParts,
|
|
118
|
+
trim: true,
|
|
119
|
+
maxLength: MEETING_LIMITS.maxIdentifierLength,
|
|
120
|
+
});
|
|
121
|
+
if (seen.has(id)) {
|
|
122
|
+
throw new apiValidation.ApiInputError(`participants[${index}] duplicates participant "${id}"`, {
|
|
123
|
+
code: 'duplicate-participant',
|
|
124
|
+
field: 'participants',
|
|
125
|
+
path: pathParts,
|
|
126
|
+
rejectedValue: participant,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (configuredAgents) {
|
|
130
|
+
if (!configuredAgents.includes(id)) {
|
|
131
|
+
throw new apiValidation.ApiInputError(
|
|
132
|
+
`participants[${index}] must name a configured agent`,
|
|
133
|
+
{
|
|
134
|
+
code: 'invalid-enum',
|
|
135
|
+
field: 'participants',
|
|
136
|
+
path: pathParts,
|
|
137
|
+
rejectedValue: participant,
|
|
138
|
+
allowedValues: configuredAgents,
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
seen.add(id);
|
|
144
|
+
participants.push(id);
|
|
145
|
+
});
|
|
146
|
+
return { title, agenda, participants };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateMeetingNote(note) {
|
|
150
|
+
return apiValidation.validateString(note, {
|
|
151
|
+
field: 'note',
|
|
152
|
+
path: 'note',
|
|
153
|
+
trim: true,
|
|
154
|
+
minLength: 1,
|
|
155
|
+
maxLength: MEETING_LIMITS.maxNoteLength,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function assertMeetingLifecycle(meeting, action) {
|
|
160
|
+
const allowedStatuses = MEETING_ACTION_STATUSES[action];
|
|
161
|
+
if (!allowedStatuses) throw new TypeError(`Unknown meeting lifecycle action: ${action}`);
|
|
162
|
+
if (!allowedStatuses.has(meeting.status)) {
|
|
163
|
+
throw new apiValidation.ApiInputError(
|
|
164
|
+
`Meeting ${meeting.id} cannot ${action} while status is ${meeting.status || '(missing)'}`,
|
|
165
|
+
{
|
|
166
|
+
code: 'meeting-state-conflict',
|
|
167
|
+
statusCode: 409,
|
|
168
|
+
field: 'status',
|
|
169
|
+
path: 'status',
|
|
170
|
+
rejectedValue: meeting.status,
|
|
171
|
+
allowedValues: [...allowedStatuses],
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return meeting;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function setMeetingRound(meeting, roundName) {
|
|
179
|
+
const status = expectedMeetingStatusForRound(roundName);
|
|
180
|
+
const round = ROUND_NUMBER_BY_NAME[String(roundName || '').toLowerCase()];
|
|
181
|
+
if (!status || !round) throw new TypeError(`Unknown meeting round: ${roundName}`);
|
|
182
|
+
meeting.status = status;
|
|
183
|
+
meeting.round = round;
|
|
184
|
+
meeting.roundStartedAt = ts();
|
|
185
|
+
return meeting;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function completeMeeting(meeting) {
|
|
189
|
+
meeting.status = MEETING_STATUS.COMPLETED;
|
|
190
|
+
meeting.completedAt = ts();
|
|
191
|
+
return meeting;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function advanceMeetingState(meeting) {
|
|
195
|
+
if (meeting.status === MEETING_STATUS.INVESTIGATING) return setMeetingRound(meeting, 'debate');
|
|
196
|
+
if (meeting.status === MEETING_STATUS.DEBATING) return setMeetingRound(meeting, 'conclude');
|
|
197
|
+
if (meeting.status === MEETING_STATUS.CONCLUDING) {
|
|
198
|
+
completeMeeting(meeting);
|
|
199
|
+
meeting.roundStartedAt = ts();
|
|
200
|
+
return meeting;
|
|
201
|
+
}
|
|
202
|
+
assertMeetingLifecycle(meeting, 'advance');
|
|
203
|
+
return meeting;
|
|
204
|
+
}
|
|
205
|
+
|
|
53
206
|
function roundKeyFor(roundName, round) {
|
|
54
207
|
const numeric = Number(round);
|
|
55
208
|
if (Number.isFinite(numeric) && numeric > 0) return String(numeric);
|
|
@@ -106,17 +259,13 @@ function buildFailedMeetingConclusion(meeting, agents, reason) {
|
|
|
106
259
|
function advanceMeetingIfRoundComplete(meeting, roundName, meetingId, config = null, pendingInboxWrites = null) {
|
|
107
260
|
if (roundName === 'investigate') {
|
|
108
261
|
if (!allParticipantsFinishedRound(meeting, roundName, meeting.round)) return false;
|
|
109
|
-
meeting
|
|
110
|
-
meeting.round = 2;
|
|
111
|
-
meeting.roundStartedAt = ts();
|
|
262
|
+
setMeetingRound(meeting, 'debate');
|
|
112
263
|
log('info', `Meeting ${meetingId}: all findings finished — advancing to debate`);
|
|
113
264
|
return true;
|
|
114
265
|
}
|
|
115
266
|
if (roundName === 'debate') {
|
|
116
267
|
if (!allParticipantsFinishedRound(meeting, roundName, meeting.round)) return false;
|
|
117
|
-
meeting
|
|
118
|
-
meeting.round = 3;
|
|
119
|
-
meeting.roundStartedAt = ts();
|
|
268
|
+
setMeetingRound(meeting, 'conclude');
|
|
120
269
|
log('info', `Meeting ${meetingId}: all debate responses finished — advancing to conclusion`);
|
|
121
270
|
return true;
|
|
122
271
|
}
|
|
@@ -125,8 +274,7 @@ function advanceMeetingIfRoundComplete(meeting, roundName, meetingId, config = n
|
|
|
125
274
|
const autoConclusion = buildFailedMeetingConclusion(meeting, agents);
|
|
126
275
|
meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt: ts() };
|
|
127
276
|
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at: ts() });
|
|
128
|
-
meeting
|
|
129
|
-
meeting.completedAt = ts();
|
|
277
|
+
completeMeeting(meeting);
|
|
130
278
|
// Defer inbox write until AFTER the mutateMeeting lock releases —
|
|
131
279
|
// writeMeetingTranscriptToInbox hits the filesystem (slug dedup, write)
|
|
132
280
|
// and must not block other writers. Matches the happy-path pattern at
|
|
@@ -489,13 +637,16 @@ function saveMeeting(meeting) {
|
|
|
489
637
|
return mutateMeeting(meeting.id, () => meeting);
|
|
490
638
|
}
|
|
491
639
|
|
|
492
|
-
function createMeeting(
|
|
640
|
+
function createMeeting(input, config, options) {
|
|
641
|
+
const validated = validateMeetingCreateInput(input, config, options);
|
|
493
642
|
const id = 'MTG-' + uid();
|
|
494
643
|
const meeting = {
|
|
495
|
-
id,
|
|
496
|
-
|
|
644
|
+
id,
|
|
645
|
+
title: validated.title,
|
|
646
|
+
agenda: validated.agenda,
|
|
647
|
+
status: MEETING_STATUS.INVESTIGATING,
|
|
497
648
|
round: 1,
|
|
498
|
-
participants: participants
|
|
649
|
+
participants: validated.participants,
|
|
499
650
|
createdBy: 'human',
|
|
500
651
|
createdAt: ts(),
|
|
501
652
|
roundStartedAt: ts(),
|
|
@@ -739,8 +890,7 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
|
|
|
739
890
|
} else if (roundName === 'conclude') {
|
|
740
891
|
meeting.conclusion = { content, agent: agentId, submittedAt: ts() };
|
|
741
892
|
meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'conclusion', content, at: ts() });
|
|
742
|
-
meeting
|
|
743
|
-
meeting.completedAt = ts();
|
|
893
|
+
completeMeeting(meeting);
|
|
744
894
|
// Defer inbox write until AFTER the lock releases — writeToInbox hits
|
|
745
895
|
// the filesystem (slug dedup, write) and must not block other writers.
|
|
746
896
|
concludedMeeting = meeting;
|
|
@@ -767,10 +917,15 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
|
|
|
767
917
|
}
|
|
768
918
|
|
|
769
919
|
function addMeetingNote(meetingId, note) {
|
|
920
|
+
const normalizedNote = validateMeetingNote(note);
|
|
921
|
+
const existing = getMeeting(meetingId);
|
|
922
|
+
if (!existing) return null;
|
|
923
|
+
assertMeetingLifecycle(existing, 'note');
|
|
770
924
|
return mutateMeeting(meetingId, (meeting) => {
|
|
771
925
|
if (!meeting) return null;
|
|
772
|
-
meeting
|
|
773
|
-
meeting.
|
|
926
|
+
assertMeetingLifecycle(meeting, 'note');
|
|
927
|
+
meeting.humanNotes.push(normalizedNote);
|
|
928
|
+
meeting.transcript.push({ round: meeting.round, agent: 'human', type: 'note', content: normalizedNote, at: ts() });
|
|
774
929
|
return meeting;
|
|
775
930
|
});
|
|
776
931
|
}
|
|
@@ -844,7 +999,8 @@ function advanceMeetingRound(meetingId) {
|
|
|
844
999
|
// already terminal. The authoritative status check still runs INSIDE the
|
|
845
1000
|
// lock below.
|
|
846
1001
|
const existing = getMeeting(meetingId);
|
|
847
|
-
if (!existing
|
|
1002
|
+
if (!existing) return null;
|
|
1003
|
+
assertMeetingLifecycle(existing, 'advance');
|
|
848
1004
|
|
|
849
1005
|
// CRITICAL: kill BEFORE acquiring the meeting lock. _killMeetingDispatches
|
|
850
1006
|
// mutates dispatch state and shells out to kill processes — never
|
|
@@ -853,13 +1009,9 @@ function advanceMeetingRound(meetingId) {
|
|
|
853
1009
|
_killMeetingDispatches(meetingId);
|
|
854
1010
|
|
|
855
1011
|
return mutateMeeting(meetingId, (meeting) => {
|
|
856
|
-
if (!meeting
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
else if (meeting.status === 'concluding') { meeting.status = 'completed'; meeting.completedAt = ts(); }
|
|
860
|
-
else return meeting; // unknown active status — no state change, but report current
|
|
861
|
-
meeting.roundStartedAt = ts();
|
|
862
|
-
return meeting;
|
|
1012
|
+
if (!meeting) return null;
|
|
1013
|
+
assertMeetingLifecycle(meeting, 'advance');
|
|
1014
|
+
return advanceMeetingState(meeting);
|
|
863
1015
|
});
|
|
864
1016
|
}
|
|
865
1017
|
|
|
@@ -868,19 +1020,23 @@ function endMeeting(meetingId) {
|
|
|
868
1020
|
// teardown / process kills never run inside our lock callback.
|
|
869
1021
|
const existing = getMeeting(meetingId);
|
|
870
1022
|
if (!existing) return null;
|
|
1023
|
+
assertMeetingLifecycle(existing, 'end');
|
|
871
1024
|
_killMeetingDispatches(meetingId);
|
|
872
1025
|
|
|
873
1026
|
return mutateMeeting(meetingId, (meeting) => {
|
|
874
1027
|
if (!meeting) return null;
|
|
875
|
-
meeting
|
|
876
|
-
|
|
877
|
-
return meeting;
|
|
1028
|
+
assertMeetingLifecycle(meeting, 'end');
|
|
1029
|
+
return completeMeeting(meeting);
|
|
878
1030
|
});
|
|
879
1031
|
}
|
|
880
1032
|
|
|
881
1033
|
function archiveMeeting(id) {
|
|
1034
|
+
const existing = getMeeting(id);
|
|
1035
|
+
if (!existing) return null;
|
|
1036
|
+
assertMeetingLifecycle(existing, 'archive');
|
|
882
1037
|
return mutateMeeting(id, (meeting) => {
|
|
883
1038
|
if (!meeting) return null;
|
|
1039
|
+
assertMeetingLifecycle(meeting, 'archive');
|
|
884
1040
|
meeting.status = MEETING_STATUS.ARCHIVED;
|
|
885
1041
|
meeting.archivedAt = ts();
|
|
886
1042
|
return meeting;
|
|
@@ -888,18 +1044,22 @@ function archiveMeeting(id) {
|
|
|
888
1044
|
}
|
|
889
1045
|
|
|
890
1046
|
function unarchiveMeeting(id) {
|
|
1047
|
+
const existing = getMeeting(id);
|
|
1048
|
+
if (!existing) return null;
|
|
1049
|
+
assertMeetingLifecycle(existing, 'unarchive');
|
|
891
1050
|
return mutateMeeting(id, (meeting) => {
|
|
892
|
-
if (!meeting
|
|
893
|
-
meeting
|
|
1051
|
+
if (!meeting) return null;
|
|
1052
|
+
assertMeetingLifecycle(meeting, 'unarchive');
|
|
1053
|
+
meeting.status = MEETING_STATUS.COMPLETED;
|
|
894
1054
|
delete meeting.archivedAt;
|
|
895
1055
|
return meeting;
|
|
896
1056
|
});
|
|
897
1057
|
}
|
|
898
1058
|
|
|
899
1059
|
function deleteMeeting(id) {
|
|
1060
|
+
if (!getMeeting(id)) return false;
|
|
900
1061
|
_killMeetingDispatches(id);
|
|
901
1062
|
const filePath = path.join(MEETINGS_DIR, id + '.json');
|
|
902
|
-
if (!fs.existsSync(filePath)) return false;
|
|
903
1063
|
try {
|
|
904
1064
|
fs.unlinkSync(filePath);
|
|
905
1065
|
} catch (e) {
|
|
@@ -1042,10 +1202,13 @@ function checkMeetingTimeouts(config) {
|
|
|
1042
1202
|
}
|
|
1043
1203
|
}
|
|
1044
1204
|
module.exports = {
|
|
1205
|
+
MEETING_LIMITS,
|
|
1045
1206
|
MEETINGS_DIR, getMeetings, summarizeMeetingsForStatus, getMeeting, saveMeeting, mutateMeeting, createMeeting,
|
|
1046
1207
|
invalidateMeetingsCache,
|
|
1047
1208
|
discoverMeetingWork, collectMeetingFindings, checkMeetingTimeouts,
|
|
1048
1209
|
addMeetingNote, advanceMeetingRound, endMeeting, archiveMeeting, unarchiveMeeting, deleteMeeting,
|
|
1210
|
+
validateMeetingId, validateMeetingCreateInput, validateMeetingNote,
|
|
1211
|
+
assertMeetingLifecycle, setMeetingRound, completeMeeting, advanceMeetingState,
|
|
1049
1212
|
EMPTY_OUTPUT_PATTERNS,
|
|
1050
1213
|
// exported for testing — engine code MUST go through
|
|
1051
1214
|
// getMeetings/discoverMeetingWork/collectMeetingFindings/checkMeetingTimeouts,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// engine/memory-retrieval.js — deterministic task-aware memory selection.
|
|
2
2
|
|
|
3
3
|
const memoryStore = require('./memory-store');
|
|
4
|
+
const reviewLearning = require('./review-learning');
|
|
4
5
|
|
|
5
6
|
const DEFAULT_TOP_K = 8;
|
|
6
7
|
const DEFAULT_MAX_BYTES = 8 * 1024;
|
|
7
8
|
const DEFAULT_CANDIDATES = 50;
|
|
9
|
+
const REVIEW_LEARNING_CANDIDATE_LIMIT = 200;
|
|
8
10
|
|
|
9
11
|
function buildTaskMemoryQuery(context = {}) {
|
|
10
12
|
const references = Array.isArray(context.references)
|
|
@@ -152,6 +154,281 @@ function retrieveRelevantMemory(context = {}, options = {}) {
|
|
|
152
154
|
};
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
// ── Reserved deterministic review-lesson recall ─────────────────────────────
|
|
158
|
+
// A dedicated project + audience recall path that never depends on an FTS hit:
|
|
159
|
+
// active review-learning lessons are listed straight from the project-scoped
|
|
160
|
+
// index (memory-store#listActiveReviewLearnings) and ranked so that exact
|
|
161
|
+
// affected-file, symbol, and tag overlap wins before lexical relevance,
|
|
162
|
+
// confidence, and recency. The pack is byte-bounded independently of the
|
|
163
|
+
// generic Relevant Memory budget and is fenced separately at the injection
|
|
164
|
+
// site. Review-learning records are globally excluded from generic recall
|
|
165
|
+
// (memory-store#searchMemoryRecords), so this is their only recall surface.
|
|
166
|
+
|
|
167
|
+
const REVIEW_FILE_PATH_RE = /(?:[\w.$@+-]+\/)+[\w.$@+-]+\.[a-z0-9]{1,8}/gi;
|
|
168
|
+
const REVIEW_BACKTICK_RE = /`([^`\n]{1,120})`/g;
|
|
169
|
+
const REVIEW_CAMEL_RE = /\b[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)+\b/g;
|
|
170
|
+
const REVIEW_CALL_RE = /\b([a-z_$][a-z0-9_$]{1,63})\s*\(/gi;
|
|
171
|
+
const REVIEW_LEX_STOP = new Set([
|
|
172
|
+
'the', 'and', 'for', 'with', 'that', 'this', 'from', 'into', 'when', 'then',
|
|
173
|
+
'have', 'has', 'was', 'are', 'not', 'but', 'you', 'your', 'all', 'any',
|
|
174
|
+
'use', 'using', 'via', 'per', 'fix', 'review', 'implement', 'task', 'work',
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
function _asArray(value) {
|
|
178
|
+
if (value == null || value === '') return [];
|
|
179
|
+
return Array.isArray(value) ? value : [value];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _reviewContextText(context) {
|
|
183
|
+
const references = _asArray(context.references).map(ref => {
|
|
184
|
+
if (typeof ref === 'string') return ref;
|
|
185
|
+
if (!ref || typeof ref !== 'object') return '';
|
|
186
|
+
return [ref.path, ref.url, ref.title, ref.type].filter(Boolean).join(' ');
|
|
187
|
+
}).join(' ');
|
|
188
|
+
return [
|
|
189
|
+
context.title,
|
|
190
|
+
context.description,
|
|
191
|
+
context.type,
|
|
192
|
+
context.failureClass,
|
|
193
|
+
context.sourcePlan,
|
|
194
|
+
references,
|
|
195
|
+
context.prTitle,
|
|
196
|
+
context.prBranch,
|
|
197
|
+
].filter(Boolean).join('\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function _reviewFilePaths(context, text) {
|
|
201
|
+
const raw = [];
|
|
202
|
+
for (const value of [..._asArray(context.files), ..._asArray(context.changedFiles)]) raw.push(value);
|
|
203
|
+
for (const ref of _asArray(context.references)) {
|
|
204
|
+
if (ref && typeof ref === 'object' && ref.path) raw.push(ref.path);
|
|
205
|
+
}
|
|
206
|
+
for (const match of text.matchAll(REVIEW_FILE_PATH_RE)) raw.push(match[0]);
|
|
207
|
+
return raw;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function _reviewSymbols(context, text) {
|
|
211
|
+
const raw = [..._asArray(context.symbols)];
|
|
212
|
+
for (const match of text.matchAll(REVIEW_BACKTICK_RE)) {
|
|
213
|
+
const inner = match[1].trim();
|
|
214
|
+
if (/^[\w.$/<>:-]+$/.test(inner)) raw.push(inner);
|
|
215
|
+
}
|
|
216
|
+
for (const match of text.matchAll(REVIEW_CAMEL_RE)) raw.push(match[0]);
|
|
217
|
+
for (const match of text.matchAll(REVIEW_CALL_RE)) raw.push(match[1]);
|
|
218
|
+
return raw;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function _reviewTags(context) {
|
|
222
|
+
const raw = [..._asArray(context.tags)];
|
|
223
|
+
if (context.type) raw.push(context.type);
|
|
224
|
+
return raw;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Derive normalized affected-file, symbol, and tag hints for any project-bound
|
|
228
|
+
// work item or PR dispatch. Normalization mirrors the capture side exactly
|
|
229
|
+
// (review-learning#normalizeReviewFacets) so hint ↔ lesson facet comparison is
|
|
230
|
+
// deterministic and case-insensitive.
|
|
231
|
+
function deriveReviewFacetHints(context = {}) {
|
|
232
|
+
const text = _reviewContextText(context);
|
|
233
|
+
return reviewLearning.normalizeReviewFacets({
|
|
234
|
+
files: _reviewFilePaths(context, text),
|
|
235
|
+
symbols: _reviewSymbols(context, text),
|
|
236
|
+
tags: _reviewTags(context),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _reviewLexTokens(text) {
|
|
241
|
+
const tokens = String(text || '').toLowerCase().match(/[a-z0-9][a-z0-9_.-]{2,63}/g) || [];
|
|
242
|
+
return new Set(tokens.filter(token => !REVIEW_LEX_STOP.has(token)));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function _overlapCount(a, b) {
|
|
246
|
+
if (!a.length || !b.length) return 0;
|
|
247
|
+
const other = new Set(b);
|
|
248
|
+
let count = 0;
|
|
249
|
+
for (const value of a) if (other.has(value)) count += 1;
|
|
250
|
+
return count;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _lessonFacets(record) {
|
|
254
|
+
try {
|
|
255
|
+
return reviewLearning.normalizeReviewFacets(record.metadata || {});
|
|
256
|
+
} catch {
|
|
257
|
+
return { files: [], symbols: [], tags: [], area: 'general' };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Rank applicable lessons: exact symbol, then file, then tag overlap dominate;
|
|
262
|
+
// lexical relevance, confidence, and recency only break ties. A lesson is
|
|
263
|
+
// applicable when it shares at least one exact facet OR any lexical token, so a
|
|
264
|
+
// lexically unrelated task touching the same file/symbol still recalls it.
|
|
265
|
+
function rankApplicableReviewLearnings(records, hints, options = {}) {
|
|
266
|
+
const hintTokens = options.hintTokens || _reviewLexTokens(options.queryText || '');
|
|
267
|
+
const scored = (Array.isArray(records) ? records : []).map(record => {
|
|
268
|
+
const facets = _lessonFacets(record);
|
|
269
|
+
const symbolOverlap = _overlapCount(hints.symbols, facets.symbols);
|
|
270
|
+
const fileOverlap = _overlapCount(hints.files, facets.files);
|
|
271
|
+
const tagOverlap = _overlapCount(hints.tags, facets.tags);
|
|
272
|
+
const lessonTokens = _reviewLexTokens(
|
|
273
|
+
`${record.title || ''}\n${record.body || ''}\n${(record.tags || []).join(' ')}`,
|
|
274
|
+
);
|
|
275
|
+
let lexical = 0;
|
|
276
|
+
for (const token of hintTokens) if (lessonTokens.has(token)) lexical += 1;
|
|
277
|
+
const confidence = Math.max(0, Math.min(1, Number(record.confidence) || 0));
|
|
278
|
+
const reasons = [];
|
|
279
|
+
if (symbolOverlap) reasons.push('exact-symbol');
|
|
280
|
+
if (fileOverlap) reasons.push('exact-file');
|
|
281
|
+
if (tagOverlap) reasons.push('exact-tag');
|
|
282
|
+
if (lexical) reasons.push('lexical');
|
|
283
|
+
const applicable = (symbolOverlap + fileOverlap + tagOverlap) > 0 || lexical > 0;
|
|
284
|
+
if (!applicable) reasons.push('not-applicable');
|
|
285
|
+
const score =
|
|
286
|
+
(symbolOverlap * 1000000) +
|
|
287
|
+
(fileOverlap * 10000) +
|
|
288
|
+
(tagOverlap * 100) +
|
|
289
|
+
Math.min(50, lexical) +
|
|
290
|
+
confidence;
|
|
291
|
+
return { record, facets, symbolOverlap, fileOverlap, tagOverlap, lexical, confidence, applicable, score, reasons };
|
|
292
|
+
});
|
|
293
|
+
return scored.sort((a, b) =>
|
|
294
|
+
b.symbolOverlap - a.symbolOverlap ||
|
|
295
|
+
b.fileOverlap - a.fileOverlap ||
|
|
296
|
+
b.tagOverlap - a.tagOverlap ||
|
|
297
|
+
b.lexical - a.lexical ||
|
|
298
|
+
b.confidence - a.confidence ||
|
|
299
|
+
Number(b.record.updatedAt || 0) - Number(a.record.updatedAt || 0) ||
|
|
300
|
+
String(a.record.id).localeCompare(String(b.record.id)));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function _formatReviewLesson(entry, bodyBytes) {
|
|
304
|
+
const record = entry.record;
|
|
305
|
+
const meta = record.metadata || {};
|
|
306
|
+
const provenance = [
|
|
307
|
+
`id=${record.id}`,
|
|
308
|
+
`audience=${meta.audience || 'project-team'}`,
|
|
309
|
+
meta.disposition ? `disposition=${meta.disposition}` : '',
|
|
310
|
+
`area=${meta.area || 'general'}`,
|
|
311
|
+
entry.facets.files.length ? `files=${entry.facets.files.join(',')}` : '',
|
|
312
|
+
entry.facets.symbols.length ? `symbols=${entry.facets.symbols.join(',')}` : '',
|
|
313
|
+
entry.facets.tags.length ? `tags=${entry.facets.tags.join(',')}` : '',
|
|
314
|
+
`date=${new Date(record.validFrom || record.createdAt || Date.now()).toISOString().slice(0, 10)}`,
|
|
315
|
+
].filter(Boolean).join(' | ');
|
|
316
|
+
const fullBody = String(record.body || '');
|
|
317
|
+
const body = _truncateUtf8(fullBody, bodyBytes);
|
|
318
|
+
const truncated = body.length < fullBody.length ? '\n\n_[excerpt truncated]_' : '';
|
|
319
|
+
return `### ${record.title}\n_${provenance}_\n\n${body}${truncated}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Bounded, deterministic recall of applicable prior review lessons. Returns a
|
|
323
|
+
// separately-packable block capped at review-learning applicableTopK records /
|
|
324
|
+
// applicableBytes, plus a bounded decision trace (stable ids, scores, reason
|
|
325
|
+
// codes) suitable for retrieval telemetry.
|
|
326
|
+
function retrieveApplicableReviewLearnings(context = {}, options = {}) {
|
|
327
|
+
const started = process.hrtime.bigint();
|
|
328
|
+
const project = String(context.project || '').trim();
|
|
329
|
+
const durationOf = () => Number(process.hrtime.bigint() - started) / 1e6;
|
|
330
|
+
const base = {
|
|
331
|
+
project,
|
|
332
|
+
hints: { files: [], symbols: [], tags: [], area: 'general' },
|
|
333
|
+
candidates: [],
|
|
334
|
+
selected: [],
|
|
335
|
+
text: '',
|
|
336
|
+
bytes: 0,
|
|
337
|
+
decisionTrace: [],
|
|
338
|
+
selectedIds: [],
|
|
339
|
+
durationMs: 0,
|
|
340
|
+
};
|
|
341
|
+
// Only dispatches with a resolved project context are eligible. Projectless
|
|
342
|
+
// Command Center and unrelated central work get no review-learning section.
|
|
343
|
+
if (!project) return { ...base, durationMs: durationOf() };
|
|
344
|
+
|
|
345
|
+
const bounds = reviewLearning.REVIEW_LEARNING_BOUNDS;
|
|
346
|
+
const topK = Math.max(1, Math.min(bounds.applicableTopK, Number(options.topK) || bounds.applicableTopK));
|
|
347
|
+
const maxBytes = Math.max(512, Math.min(bounds.applicableBytes, Number(options.maxBytes) || bounds.applicableBytes));
|
|
348
|
+
const hints = deriveReviewFacetHints(context);
|
|
349
|
+
const agent = String(context.agent || '').trim().toLowerCase();
|
|
350
|
+
|
|
351
|
+
let lessons;
|
|
352
|
+
try {
|
|
353
|
+
lessons = memoryStore.listActiveReviewLearnings({
|
|
354
|
+
project,
|
|
355
|
+
limit: Number(options.candidateLimit) || REVIEW_LEARNING_CANDIDATE_LIMIT,
|
|
356
|
+
});
|
|
357
|
+
} catch {
|
|
358
|
+
return { ...base, hints, durationMs: durationOf() };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Audience gate: project-team lessons reach any agent in the same project;
|
|
362
|
+
// author-only lessons require the matching author agent. Project scoping is
|
|
363
|
+
// already enforced by listActiveReviewLearnings, so names/paths never leak
|
|
364
|
+
// across projects.
|
|
365
|
+
const audienceFiltered = (lessons || []).filter(record => {
|
|
366
|
+
const audience = String(record.metadata?.audience || 'project-team').toLowerCase();
|
|
367
|
+
if (audience === 'author') {
|
|
368
|
+
return !!agent && String(record.metadata?.authorAgent || '').toLowerCase() === agent;
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const hintTokens = _reviewLexTokens(_reviewContextText(context));
|
|
374
|
+
const ranked = rankApplicableReviewLearnings(audienceFiltered, hints, { hintTokens });
|
|
375
|
+
|
|
376
|
+
const selected = [];
|
|
377
|
+
const decisionTrace = [];
|
|
378
|
+
let used = 0;
|
|
379
|
+
let rank = 0;
|
|
380
|
+
for (const entry of ranked) {
|
|
381
|
+
const traceEntry = {
|
|
382
|
+
id: entry.record.id,
|
|
383
|
+
rank,
|
|
384
|
+
score: Number(entry.score.toFixed(4)),
|
|
385
|
+
selected: false,
|
|
386
|
+
reasons: entry.reasons.slice(),
|
|
387
|
+
};
|
|
388
|
+
rank += 1;
|
|
389
|
+
if (!entry.applicable) {
|
|
390
|
+
decisionTrace.push(traceEntry);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (selected.length >= topK) {
|
|
394
|
+
traceEntry.reasons.push('record-cap');
|
|
395
|
+
decisionTrace.push(traceEntry);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const remaining = maxBytes - used;
|
|
399
|
+
if (remaining < 128) {
|
|
400
|
+
traceEntry.reasons.push('byte-cap');
|
|
401
|
+
decisionTrace.push(traceEntry);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const block = _formatReviewLesson(entry, Math.max(96, remaining - 96));
|
|
405
|
+
const separator = selected.length ? '\n\n---\n\n' : '';
|
|
406
|
+
const bytes = Buffer.byteLength(separator + block, 'utf8');
|
|
407
|
+
if (bytes > remaining) {
|
|
408
|
+
traceEntry.reasons.push('byte-cap');
|
|
409
|
+
decisionTrace.push(traceEntry);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
used += bytes;
|
|
413
|
+
selected.push({ record: entry.record, block });
|
|
414
|
+
traceEntry.selected = true;
|
|
415
|
+
decisionTrace.push(traceEntry);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const text = selected.map(item => item.block).join('\n\n---\n\n');
|
|
419
|
+
return {
|
|
420
|
+
project,
|
|
421
|
+
hints,
|
|
422
|
+
candidates: ranked,
|
|
423
|
+
selected: selected.map(item => item.record),
|
|
424
|
+
text,
|
|
425
|
+
bytes: used,
|
|
426
|
+
decisionTrace,
|
|
427
|
+
selectedIds: selected.map(item => item.record.id),
|
|
428
|
+
durationMs: durationOf(),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
155
432
|
module.exports = {
|
|
156
433
|
DEFAULT_TOP_K,
|
|
157
434
|
DEFAULT_MAX_BYTES,
|
|
@@ -161,5 +438,8 @@ module.exports = {
|
|
|
161
438
|
dedupeMemoryCandidates,
|
|
162
439
|
packMemoryResults,
|
|
163
440
|
retrieveRelevantMemory,
|
|
441
|
+
deriveReviewFacetHints,
|
|
442
|
+
rankApplicableReviewLearnings,
|
|
443
|
+
retrieveApplicableReviewLearnings,
|
|
164
444
|
_truncateUtf8,
|
|
165
445
|
};
|