@yemi33/minions 0.1.2372 → 0.1.2374
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/dashboard/js/settings.js +1 -0
- package/dashboard.js +127 -112
- package/docs/design-inbox-entries-schema.md +106 -0
- package/docs/live-checkout-mode.md +27 -0
- package/docs/runtime-adapters.md +103 -0
- package/engine/acp-transport.js +594 -0
- package/engine/agent-worker-pool.js +367 -0
- package/engine/cc-worker-pool.js +38 -563
- package/engine/cleanup.js +34 -19
- package/engine/cli.js +19 -0
- package/engine/db/migrations/019-inbox-entries.js +37 -0
- package/engine/inbox-store.js +276 -0
- package/engine/live-checkout.js +168 -0
- package/engine/pooled-agent-process.js +158 -0
- package/engine/runtimes/copilot.js +5 -0
- package/engine/shared.js +75 -1
- package/engine/timeout.js +51 -7
- package/engine.js +81 -7
- package/package.json +1 -1
package/dashboard/js/settings.js
CHANGED
|
@@ -1132,6 +1132,7 @@ async function saveSettings() {
|
|
|
1132
1132
|
liveCheckoutAutoStash: !!document.getElementById('set-liveCheckoutAutoStash')?.checked,
|
|
1133
1133
|
liveCheckoutAutoReset: !!document.getElementById('set-liveCheckoutAutoReset')?.checked,
|
|
1134
1134
|
liveCheckoutAutoBaseRepair: !!document.getElementById('set-liveCheckoutAutoBaseRepair')?.checked,
|
|
1135
|
+
liveCheckoutAutoFreshen: !!document.getElementById('set-liveCheckoutAutoFreshen')?.checked,
|
|
1135
1136
|
orphanHolderScanTimeoutMs: document.getElementById('set-orphanHolderScanTimeoutMs')?.value,
|
|
1136
1137
|
statusProbeKillTimeoutMs: document.getElementById('set-statusProbeKillTimeoutMs')?.value,
|
|
1137
1138
|
adoPollEnabled: document.getElementById('set-adoPollEnabled').checked,
|
package/dashboard.js
CHANGED
|
@@ -7854,23 +7854,35 @@ const server = http.createServer(async (req, res) => {
|
|
|
7854
7854
|
return jsonReply(res, 200, _plansCache);
|
|
7855
7855
|
}
|
|
7856
7856
|
const fsp = fs.promises;
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
//
|
|
7864
|
-
|
|
7865
|
-
const centralWi = require('./engine/work-items-store').readWorkItemsForScope('central');
|
|
7857
|
+
// W-mrffmjaf002f08f0 — PRD JSON is SQL-authoritative (engine/prd-store.js);
|
|
7858
|
+
// no PRD JSON file is guaranteed to exist on disk post SQL-only cutover
|
|
7859
|
+
// (migration 018), so PRD plan cards AND conversion state are read from
|
|
7860
|
+
// prdStore.listPrdRows() below, not fs.readdir(PRD_DIR). This is also what
|
|
7861
|
+
// fixes the underlying bug: a completed SQL PRD's source_plan must mark
|
|
7862
|
+
// its Markdown draft "converted" even once the plan-to-prd work item that
|
|
7863
|
+
// produced it has aged out of central work-item retention.
|
|
7864
|
+
const prdRows = prdStore.listPrdRows();
|
|
7866
7865
|
const completedPrdFiles = new Set(
|
|
7867
|
-
|
|
7868
|
-
.map(w => w.planFile)
|
|
7866
|
+
prdRows.filter(r => r.plan && r.plan.source_plan).map(r => r.plan.source_plan)
|
|
7869
7867
|
);
|
|
7868
|
+
// Defense-in-depth union with the legacy central-WI signal, in case a
|
|
7869
|
+
// conversion work item completed but its PRD row somehow lacks
|
|
7870
|
+
// source_plan; SQL PRD rows above remain the primary, canonical source.
|
|
7871
|
+
const centralWi = require('./engine/work-items-store').readWorkItemsForScope('central');
|
|
7872
|
+
for (const w of centralWi) {
|
|
7873
|
+
if (w.type === WORK_TYPE.PLAN_TO_PRD && DONE_STATUSES.has(w.status) && w.planFile) {
|
|
7874
|
+
completedPrdFiles.add(w.planFile);
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
|
|
7870
7878
|
const plans = [];
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7879
|
+
|
|
7880
|
+
// --- Markdown source plans (plans/*.md + plans/archive/*.md, file-backed) ---
|
|
7881
|
+
for (const { dir, archived } of [
|
|
7882
|
+
{ dir: PLANS_DIR, archived: false },
|
|
7883
|
+
{ dir: path.join(PLANS_DIR, 'archive'), archived: true },
|
|
7884
|
+
]) {
|
|
7885
|
+
const allFiles = (await fsp.readdir(dir).catch(() => [])).filter(f => f.endsWith('.md'));
|
|
7874
7886
|
const dirResults = await Promise.all(allFiles.map(async f => {
|
|
7875
7887
|
const filePath = path.join(dir, f);
|
|
7876
7888
|
const [content, stat] = await Promise.all([
|
|
@@ -7878,107 +7890,110 @@ const server = http.createServer(async (req, res) => {
|
|
|
7878
7890
|
fsp.stat(filePath).catch(() => null),
|
|
7879
7891
|
]);
|
|
7880
7892
|
const updatedAt = stat ? new Date(stat.mtimeMs).toISOString() : '';
|
|
7881
|
-
const
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
// pills in the plan card meta line (dashboard/js/render-plans.js
|
|
7912
|
-
// renderPlanCard). Group missing_features by item.project (with
|
|
7913
|
-
// fallback to plan.project) and count `status === 'done'` per
|
|
7914
|
-
// bucket. Keys are first-seen order so the rendered pill row is
|
|
7915
|
-
// stable. Always emitted on PRD JSON records (possibly empty);
|
|
7916
|
-
// MD drafts omit it since they carry no item-level statuses.
|
|
7917
|
-
const _perProjectProgress = {};
|
|
7918
|
-
for (const it of (plan.missing_features || [])) {
|
|
7919
|
-
const proj = (it && it.project) || plan.project || '';
|
|
7920
|
-
if (!proj) continue;
|
|
7921
|
-
if (!_perProjectProgress[proj]) _perProjectProgress[proj] = { complete: 0, total: 0 };
|
|
7922
|
-
_perProjectProgress[proj].total += 1;
|
|
7923
|
-
if (it && it.status === 'done') _perProjectProgress[proj].complete += 1;
|
|
7924
|
-
}
|
|
7925
|
-
return {
|
|
7926
|
-
file: f, format: 'prd', archived,
|
|
7927
|
-
project: plan.project || '',
|
|
7928
|
-
_projects,
|
|
7929
|
-
_perProjectProgress,
|
|
7930
|
-
summary: plan.plan_summary || '',
|
|
7931
|
-
status,
|
|
7932
|
-
branchStrategy: plan.branch_strategy || 'parallel',
|
|
7933
|
-
featureBranch: plan.feature_branch || '',
|
|
7934
|
-
itemCount: (plan.missing_features || []).length,
|
|
7935
|
-
generatedBy: plan.generated_by || '',
|
|
7936
|
-
generatedAt: plan.generated_at || '',
|
|
7937
|
-
completedAt: plan.completedAt || '',
|
|
7938
|
-
updatedAt,
|
|
7939
|
-
requiresApproval: plan.requires_approval || false,
|
|
7940
|
-
revisionFeedback: plan.revision_feedback || null,
|
|
7941
|
-
sourcePlan: plan.source_plan || null,
|
|
7942
|
-
archiveReady: plan._archiveReady || false,
|
|
7943
|
-
archiveReadyAt: plan._archiveReadyAt || null,
|
|
7944
|
-
planStale: freshStale || plan.planStale || false,
|
|
7945
|
-
};
|
|
7946
|
-
} catch { return null; /* JSON parse fallback */ }
|
|
7947
|
-
} else {
|
|
7948
|
-
const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
|
|
7949
|
-
const projectMatch = content.match(/\*\*Project:\*\*\s*(.+)/m);
|
|
7950
|
-
const authorMatch = content.match(/\*\*Author:\*\*\s*(.+)/m);
|
|
7951
|
-
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/m);
|
|
7952
|
-
const versionMatch = f.match(/-v(\d+)/);
|
|
7953
|
-
// P-e8d49105 — for MD drafts, the cross-repo marker / **Projects:**
|
|
7954
|
-
// line is the source of truth (parsed by shared.extractPlanTargetProjects).
|
|
7955
|
-
// If neither is present, fall back to the singular **Project:** header
|
|
7956
|
-
// so single-project drafts render the same badge as today.
|
|
7957
|
-
const declaredProject = projectMatch ? projectMatch[1].trim() : '';
|
|
7958
|
-
const targetProjects = shared.extractPlanTargetProjects(content);
|
|
7959
|
-
const _projects = targetProjects.length > 0
|
|
7960
|
-
? targetProjects
|
|
7961
|
-
: (declaredProject ? [declaredProject] : []);
|
|
7962
|
-
return {
|
|
7963
|
-
file: f, format: 'draft', archived,
|
|
7964
|
-
project: declaredProject,
|
|
7965
|
-
_projects,
|
|
7966
|
-
summary: titleMatch ? titleMatch[1].trim() : f.replace('.md', ''),
|
|
7967
|
-
status: archived ? 'completed' : completedPrdFiles.has(f) ? 'converted' : 'draft',
|
|
7968
|
-
branchStrategy: '',
|
|
7969
|
-
featureBranch: '',
|
|
7970
|
-
itemCount: (content.match(/^\d+\.\s+\*\*/gm) || []).length,
|
|
7971
|
-
generatedBy: authorMatch ? authorMatch[1].trim() : '',
|
|
7972
|
-
generatedAt: dateMatch ? dateMatch[1].trim() : '',
|
|
7973
|
-
updatedAt,
|
|
7974
|
-
requiresApproval: false,
|
|
7975
|
-
revisionFeedback: null,
|
|
7976
|
-
version: versionMatch ? parseInt(versionMatch[1]) : null,
|
|
7977
|
-
};
|
|
7978
|
-
}
|
|
7893
|
+
const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
|
|
7894
|
+
const projectMatch = content.match(/\*\*Project:\*\*\s*(.+)/m);
|
|
7895
|
+
const authorMatch = content.match(/\*\*Author:\*\*\s*(.+)/m);
|
|
7896
|
+
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/m);
|
|
7897
|
+
const versionMatch = f.match(/-v(\d+)/);
|
|
7898
|
+
// P-e8d49105 — for MD drafts, the cross-repo marker / **Projects:**
|
|
7899
|
+
// line is the source of truth (parsed by shared.extractPlanTargetProjects).
|
|
7900
|
+
// If neither is present, fall back to the singular **Project:** header
|
|
7901
|
+
// so single-project drafts render the same badge as today.
|
|
7902
|
+
const declaredProject = projectMatch ? projectMatch[1].trim() : '';
|
|
7903
|
+
const targetProjects = shared.extractPlanTargetProjects(content);
|
|
7904
|
+
const _projects = targetProjects.length > 0
|
|
7905
|
+
? targetProjects
|
|
7906
|
+
: (declaredProject ? [declaredProject] : []);
|
|
7907
|
+
return {
|
|
7908
|
+
file: f, format: 'draft', archived,
|
|
7909
|
+
project: declaredProject,
|
|
7910
|
+
_projects,
|
|
7911
|
+
summary: titleMatch ? titleMatch[1].trim() : f.replace('.md', ''),
|
|
7912
|
+
status: archived ? 'completed' : completedPrdFiles.has(f) ? 'converted' : 'draft',
|
|
7913
|
+
branchStrategy: '',
|
|
7914
|
+
featureBranch: '',
|
|
7915
|
+
itemCount: (content.match(/^\d+\.\s+\*\*/gm) || []).length,
|
|
7916
|
+
generatedBy: authorMatch ? authorMatch[1].trim() : '',
|
|
7917
|
+
generatedAt: dateMatch ? dateMatch[1].trim() : '',
|
|
7918
|
+
updatedAt,
|
|
7919
|
+
requiresApproval: false,
|
|
7920
|
+
revisionFeedback: null,
|
|
7921
|
+
version: versionMatch ? parseInt(versionMatch[1]) : null,
|
|
7922
|
+
};
|
|
7979
7923
|
}));
|
|
7980
7924
|
for (const r of dirResults) if (r) plans.push(r);
|
|
7981
7925
|
}
|
|
7926
|
+
|
|
7927
|
+
// --- PRD JSON records (prd/*.json + prd/archive/*.json, SQL-backed) ---
|
|
7928
|
+
for (const row of prdRows) {
|
|
7929
|
+
const plan = row.plan;
|
|
7930
|
+
if (!plan || typeof plan !== 'object') continue;
|
|
7931
|
+
const archived = !!row.archived;
|
|
7932
|
+
const f = row.filename;
|
|
7933
|
+
const updatedAt = row.updatedAt ? new Date(row.updatedAt).toISOString() : '';
|
|
7934
|
+
const status = plan.status || (plan.requires_approval && !plan.approvedAt ? 'awaiting-approval' : 'active');
|
|
7935
|
+
// W-mqacrzis0003df4a Bug 2 — fresh staleness from plans/*.md mtime.
|
|
7936
|
+
// plan.planStale lags by an engine tick; stat the source plan here
|
|
7937
|
+
// so the Plans tab does not silently mirror a stale-but-cached false.
|
|
7938
|
+
let freshStale = false;
|
|
7939
|
+
if (!archived && plan.source_plan) {
|
|
7940
|
+
try {
|
|
7941
|
+
const sourceMtime = Math.floor(fs.statSync(path.join(PLANS_DIR, plan.source_plan)).mtimeMs);
|
|
7942
|
+
const recorded = plan.sourcePlanModifiedAt ? new Date(plan.sourcePlanModifiedAt).getTime() : null;
|
|
7943
|
+
if (recorded && sourceMtime > recorded) freshStale = true;
|
|
7944
|
+
} catch { /* source plan may have been deleted/renamed */ }
|
|
7945
|
+
}
|
|
7946
|
+
// P-e8d49105 — _projects rollup feeds the plan-card multi-badge
|
|
7947
|
+
// path in dashboard/js/render-plans.js. For PRD JSONs, walk
|
|
7948
|
+
// missing_features[].project (order-preserving dedup) and fall
|
|
7949
|
+
// back to plan.project for items that omit it. Single-project
|
|
7950
|
+
// plans collapse to a single-element array, identical to today's
|
|
7951
|
+
// single `p.project` badge.
|
|
7952
|
+
const _projects = [];
|
|
7953
|
+
const _seen = new Set();
|
|
7954
|
+
for (const it of (plan.missing_features || [])) {
|
|
7955
|
+
const proj = (it && it.project) || plan.project || '';
|
|
7956
|
+
if (proj && !_seen.has(proj)) { _seen.add(proj); _projects.push(proj); }
|
|
7957
|
+
}
|
|
7958
|
+
if (_projects.length === 0 && plan.project) _projects.push(plan.project);
|
|
7959
|
+
// P-66b1faec — _perProjectProgress feeds the per-project status
|
|
7960
|
+
// pills in the plan card meta line (dashboard/js/render-plans.js
|
|
7961
|
+
// renderPlanCard). Group missing_features by item.project (with
|
|
7962
|
+
// fallback to plan.project) and count `status === 'done'` per
|
|
7963
|
+
// bucket. Keys are first-seen order so the rendered pill row is
|
|
7964
|
+
// stable. Always emitted on PRD JSON records (possibly empty);
|
|
7965
|
+
// MD drafts omit it since they carry no item-level statuses.
|
|
7966
|
+
const _perProjectProgress = {};
|
|
7967
|
+
for (const it of (plan.missing_features || [])) {
|
|
7968
|
+
const proj = (it && it.project) || plan.project || '';
|
|
7969
|
+
if (!proj) continue;
|
|
7970
|
+
if (!_perProjectProgress[proj]) _perProjectProgress[proj] = { complete: 0, total: 0 };
|
|
7971
|
+
_perProjectProgress[proj].total += 1;
|
|
7972
|
+
if (it && it.status === 'done') _perProjectProgress[proj].complete += 1;
|
|
7973
|
+
}
|
|
7974
|
+
plans.push({
|
|
7975
|
+
file: f, format: 'prd', archived,
|
|
7976
|
+
project: plan.project || '',
|
|
7977
|
+
_projects,
|
|
7978
|
+
_perProjectProgress,
|
|
7979
|
+
summary: plan.plan_summary || '',
|
|
7980
|
+
status,
|
|
7981
|
+
branchStrategy: plan.branch_strategy || 'parallel',
|
|
7982
|
+
featureBranch: plan.feature_branch || '',
|
|
7983
|
+
itemCount: (plan.missing_features || []).length,
|
|
7984
|
+
generatedBy: plan.generated_by || '',
|
|
7985
|
+
generatedAt: plan.generated_at || '',
|
|
7986
|
+
completedAt: plan.completedAt || '',
|
|
7987
|
+
updatedAt,
|
|
7988
|
+
requiresApproval: plan.requires_approval || false,
|
|
7989
|
+
revisionFeedback: plan.revision_feedback || null,
|
|
7990
|
+
sourcePlan: plan.source_plan || null,
|
|
7991
|
+
archiveReady: plan._archiveReady || false,
|
|
7992
|
+
archiveReadyAt: plan._archiveReadyAt || null,
|
|
7993
|
+
planStale: freshStale || plan.planStale || false,
|
|
7994
|
+
});
|
|
7995
|
+
}
|
|
7996
|
+
|
|
7982
7997
|
plans.sort((a, b) => (b.generatedAt || '').localeCompare(a.generatedAt || ''));
|
|
7983
7998
|
_plansCache = plans;
|
|
7984
7999
|
_plansCacheTs = Date.now();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# inbox_entries Table Schema
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The `inbox_entries` table is the persistent store for operator and agent inbox messages within the Minions orchestration engine. It provides a time-ordered, unread-tracking message queue decoupled from the runtime work-item/dispatch lifecycle.
|
|
5
|
+
|
|
6
|
+
## Schema Specification
|
|
7
|
+
|
|
8
|
+
### Column Definitions
|
|
9
|
+
|
|
10
|
+
| Column | Type | Constraints | Purpose |
|
|
11
|
+
|--------|------|-----------|---------|
|
|
12
|
+
| `id` | TEXT | PRIMARY KEY | Unique message identifier; generated as `<recipient>-<timestamp>-<sender>-<seq>` format |
|
|
13
|
+
| `recipient_session_id` | TEXT | NOT NULL | Session ID of the inbox owner (operator or agent); routes messages to specific actor |
|
|
14
|
+
| `sender_id` | TEXT | NOT NULL | Identifier of the message originator; structured as `<agent-id>` or `'system'` or `'operator'` |
|
|
15
|
+
| `sender_name` | TEXT | NOT NULL | Human-readable sender name for UI display; persisted at write-time for offline/archived reference |
|
|
16
|
+
| `sender_type` | TEXT | NOT NULL | Enum: `'agent'`, `'system'`, `'operator'`; constrains allowed sender_id values |
|
|
17
|
+
| `interaction_id` | TEXT | NOT NULL | Group ID linking related messages (e.g., work-item dispatch feedback, PR review batch); enables threading and deduplication |
|
|
18
|
+
| `sequence` | INTEGER | NOT NULL, DEFAULT 0 | Per-interaction message counter; disambiguates bursts and enables ordering within same millisecond |
|
|
19
|
+
| `summary` | TEXT | NOT NULL | Title/subject line; used for list views and user scanning; always present even if content is truncated |
|
|
20
|
+
| `content` | TEXT | NOT NULL | Full message body; may contain Markdown, code blocks, or structured JSON sidecars (not yet parsed at table level) |
|
|
21
|
+
| `unread` | INTEGER | NOT NULL, DEFAULT 1 | Boolean flag (0/1); toggled by reader, cleared on archive/delete; enables fast unread-count queries |
|
|
22
|
+
| `sent_at` | INTEGER | NOT NULL | Unix timestamp (milliseconds) of message creation; primary sort key for time-ordered display |
|
|
23
|
+
| `read_at` | INTEGER | Nullable | Unix timestamp (milliseconds) when recipient first read the message; null until marked read |
|
|
24
|
+
| `notified_at` | INTEGER | Nullable | Unix timestamp (milliseconds) of last notification event sent to operator; null if no notification dispatched |
|
|
25
|
+
|
|
26
|
+
### Constraints & Relationships
|
|
27
|
+
|
|
28
|
+
- **PK constraint:** `id` is globally unique across all inboxes; collision risk is negligible given the composite structure (recipient + timestamp + sender + seq).
|
|
29
|
+
- **FK (soft):** `recipient_session_id` does NOT enforce a referential constraint to `sessions` table (sessions may be archived). Queries SHOULD handle missing recipients gracefully.
|
|
30
|
+
- **FK (soft):** `sender_id` is informational only; no validation that the sender exists in the `config.agents` or active sessions. Invalid senders are logged but do not block writes.
|
|
31
|
+
- **No composite index on (recipient_session_id, sent_at):** See Performance section.
|
|
32
|
+
|
|
33
|
+
## Architectural Rationale
|
|
34
|
+
|
|
35
|
+
### Design Decisions
|
|
36
|
+
|
|
37
|
+
1. **Separate inbox table vs. inline dispatch/work-item attachments.**
|
|
38
|
+
- Inboxes can outlive their source work items (e.g., a feedback message remains useful after the WI is done).
|
|
39
|
+
- Decoupling allows independent inbox lifecycle (archive, export, cleanup) without cascading work-item deletes.
|
|
40
|
+
- Supports rich async consolidation (team notes, KB articles) that don't belong to any single WI.
|
|
41
|
+
|
|
42
|
+
2. **`unread` flag instead of soft-delete or status enum.**
|
|
43
|
+
- Fast count queries for badge (`SELECT COUNT(*) WHERE recipient_session_id = ? AND unread = 1`).
|
|
44
|
+
- Unread state is mutable; allows mark-as-read UX without data loss.
|
|
45
|
+
- Simplifies archive/export (no tombstone filtering needed).
|
|
46
|
+
|
|
47
|
+
3. **`sender_type` as an explicit column.**
|
|
48
|
+
- Allows schema validation (e.g., "agent" senders must have valid IDs; "system" is always valid).
|
|
49
|
+
- Enables filtered views ("show only agent messages" or "system alerts only") without string parsing.
|
|
50
|
+
|
|
51
|
+
4. **`interaction_id` for grouping.**
|
|
52
|
+
- Enables undo/redo on related messages (e.g., canceling all build-failure notifications from one dispatch).
|
|
53
|
+
- Supports consolidation: multiple small notifications can be collapsed into one summary message.
|
|
54
|
+
|
|
55
|
+
5. **Persistent `sender_name` at write-time.**
|
|
56
|
+
- Operator may rename agents or disable them; storing the name ensures historical accuracy.
|
|
57
|
+
- UI doesn't need to perform runtime lookups on archived inboxes.
|
|
58
|
+
|
|
59
|
+
6. **Unix timestamps (milliseconds) instead of ISO strings.**
|
|
60
|
+
- Enables range queries (`sent_at BETWEEN ? AND ?`) without parsing.
|
|
61
|
+
- Smaller column footprint; easier to sort.
|
|
62
|
+
- Downside: not human-readable; UI must convert to locale strings.
|
|
63
|
+
|
|
64
|
+
### Entity Relationships
|
|
65
|
+
|
|
66
|
+
- **inbox_entries ← dispatch WI feedback:** A failed dispatch may create multiple inbox entries (error summary, retry recommendation, logs snippet). Threaded via `interaction_id`.
|
|
67
|
+
- **inbox_entries ← PR reviews:** Humans and auto-review agents post comments; summarized as inbox alerts to project owners via `interaction_id` per-PR.
|
|
68
|
+
- **inbox_entries ← consolidation:** `engine/consolidation.js` reads inbox entries to extract `agent:` frontmatter and route findings into per-agent knowledge files.
|
|
69
|
+
|
|
70
|
+
## Performance Considerations
|
|
71
|
+
|
|
72
|
+
### Indexing Strategy
|
|
73
|
+
|
|
74
|
+
**Current indexes (recommended):**
|
|
75
|
+
- `idx_recipient_sent` on `(recipient_session_id, sent_at DESC)` — enables efficient "unread count" and "latest N messages" queries without table scans.
|
|
76
|
+
- `idx_interaction` on `(interaction_id)` — enables grouping/dedup queries (e.g., "was this interaction already notified?").
|
|
77
|
+
- PK `id` is implicit index.
|
|
78
|
+
|
|
79
|
+
**Query patterns:**
|
|
80
|
+
- **Unread count per session:** `SELECT COUNT(*) WHERE recipient_session_id = ? AND unread = 1` → must scan `idx_recipient_sent` to avoid full table.
|
|
81
|
+
- **Latest messages:** `SELECT * WHERE recipient_session_id = ? ORDER BY sent_at DESC LIMIT 20` → seeks on `idx_recipient_sent` DESC.
|
|
82
|
+
- **Thread view:** `SELECT * WHERE interaction_id = ? ORDER BY sent_at` → seeks on `idx_interaction`, then sorts in application or via secondary index.
|
|
83
|
+
- **Cleanup:** `DELETE WHERE sent_at < ? AND unread = 0` → full table scan (acceptable; cleanup is infrequent, low-priority background task).
|
|
84
|
+
|
|
85
|
+
**Missing indexes (future optimization):**
|
|
86
|
+
- `idx_sender_id` — if queries like "messages from this agent" are common, consider adding.
|
|
87
|
+
- `idx_unread` — if badge ("N unread") is a hot query, consider combining with recipient (`idx_recipient_unread` on `(recipient_session_id, unread)`).
|
|
88
|
+
|
|
89
|
+
### Size Estimates & Cleanup
|
|
90
|
+
|
|
91
|
+
- **Row size:** ~500–2000 bytes (summary + content; content can be verbose with logs/code snippets).
|
|
92
|
+
- **Growth:** ~5–50 entries/day (depends on agent activity and verbose flagging).
|
|
93
|
+
- **Retention:** No automatic cleanup; operators may archive/export and manually delete aged entries. Engine does NOT auto-prune.
|
|
94
|
+
- **Recommended**: Implement a dashboard setting to soft-delete entries older than N days (default 30) with an undo/export option.
|
|
95
|
+
|
|
96
|
+
## Migration & Backward Compatibility
|
|
97
|
+
|
|
98
|
+
- **Migration 018 or later** creates `inbox_entries` as part of SQL state cutover. Legacy `inbox/` Markdown files are NOT imported; operators must re-send or manually recreate critical messages.
|
|
99
|
+
- **No schema versioning needed** for now; table is immutable-append-only (reads/writes only).
|
|
100
|
+
|
|
101
|
+
## Related Code
|
|
102
|
+
|
|
103
|
+
- **Writes:** `engine/inbox-store.js` (primary mutator; uses `getDb()` and `emitStateEvent`).
|
|
104
|
+
- **Reads:** `engine/queries.js#getInboxForSession()`, dashboard `/api/inbox`, `engine/consolidation.js`.
|
|
105
|
+
- **UI:** `dashboard/pages/inbox.html`, `dashboard/js/inbox.js`.
|
|
106
|
+
|
|
@@ -153,6 +153,33 @@ A count `> 0` returns `{ ok:false, reason:'stale-main', mainRef, ahead, localMai
|
|
|
153
153
|
|
|
154
154
|
`spawnAgent` completes the dispatch non-retryably with the dedicated **`FAILURE_CLASS.LIVE_CHECKOUT_STALE_BASE`** (`'live-checkout-stale-base'`; in `dispatch.js`'s `neverRetry` set) plus a `live-checkout-stale-base-<wi-id>` inbox alert and a `_pendingReason: 'live_checkout_stale_base'` stamp — instead of `LIVE_CHECKOUT_FAILED` (retryable) retry-storming an identical structural failure to `maxRetries`. The engine **never** resets, cleans, or rebases the operator's local `mainRef` — surfacing it as a refusal keeps history rewrites in the operator's hands, consistent with the dirty and mid-operation refusals. The alert's recovery guidance offers two paths: salvage the extra commits onto their own branch then `git reset --hard origin/<mainRef>`, or discard them with the same reset — either leaves `mainRef` matching `origin/<mainRef>` so the next dispatch forks off a clean base.
|
|
155
155
|
|
|
156
|
+
#### 6b. Opt-in auto-freshen (`liveCheckoutAutoFreshen`, W-mrdlcud2000e630e)
|
|
157
|
+
|
|
158
|
+
The STALE-BASE GUARD (§6) only counts the **local-ahead** side: local `mainRef` merely being **behind** `origin/<mainRef>` (ahead == 0, behind > 0) is silently allowed to fork off a stale-but-safe base. Issue #226 is the reason the engine never auto-fetches live checkouts — preserving operator control over history rewrites. **Auto-freshen** is the narrow, opt-in exception that lets a live (or hybrid liveValidation) dispatch work off the freshest safe base without ever risking a history rewrite or data loss.
|
|
159
|
+
|
|
160
|
+
Enabling **`liveCheckoutAutoFreshen`** (per-project `project.liveCheckoutAutoFreshen` > fleet-wide `engine.liveCheckoutAutoFreshen` > **`false`**, resolved by `shared.resolveLiveCheckoutAutoFreshen`) makes `prepareLiveCheckout` — **before** the STALE-BASE GUARD, as `_maybeAutoFreshenLocalMain` — fast-forward the local base when, and **only** when, **all** of these hold:
|
|
161
|
+
|
|
162
|
+
- the tree is **clean** (the Guarantee 2 dirty-tree bail already ran — auto-freshen is never attempted on a dirty tree, and the mid-operation preflight of Guarantee 5 has already refused any in-progress op);
|
|
163
|
+
- HEAD is on `mainRef` (the branch it is about to fork off);
|
|
164
|
+
- this is **not** an `allowNonMainBase` dispatch (PR-targeted fixes / shared-branch continuations are exempt — same skip condition as §6/§7);
|
|
165
|
+
- local `mainRef` has **ZERO** commits ahead of `origin/<mainRef>` — i.e. **not** the stale-base-with-unpushed-commits case; that path stays UNTOUCHED and still hits §6's refusal;
|
|
166
|
+
- local `mainRef` **is** behind `origin/<mainRef>` (behind > 0).
|
|
167
|
+
|
|
168
|
+
When they hold it runs the **one sanctioned live-mode fetch** — `git fetch origin <mainRef>` — then a **fast-forward-only** update, `git merge --ff-only refs/remotes/origin/<mainRef>` (HEAD is on `mainRef`, so this advances the checked-out base). A `--ff-only` update can never rewrite history or drop local commits (there are none ahead), so the no-data-loss guarantee is preserved even though issue #226's blanket no-fetch rule is relaxed for this one case.
|
|
169
|
+
|
|
170
|
+
- **Fails open.** Any fetch/network/auth failure, an uncomputable divergence, or an unexpected `--ff-only` refusal (e.g. a race committed onto local `mainRef`) returns **without mutating**, so the dispatch proceeds to the existing stale-base check exactly as if auto-freshen were off. A freshen-step hiccup never hard-fails the dispatch.
|
|
171
|
+
- **Low-noise audit.** On a successful freshen it logs an `info` line and writes a `live-checkout-autofreshen-<wi-id>` inbox note recording the before/after local `mainRef` SHA — **only** when a freshen actually happened (never on every dispatch), so it does not alert-spam.
|
|
172
|
+
- **Uniform across live and hybrid.** Hybrid mode reuses the same live-checkout preflight for its `liveValidation` dispatch path, so the flag applies to both without extra wiring.
|
|
173
|
+
- **Scope-narrow.** The ahead>0 STALE-BASE GUARD refusal is deliberately **unchanged**; auto-freshen only adds a safe fast-forward for the ahead==0/behind>0 case. Default **OFF** — a human opts individual projects in.
|
|
174
|
+
|
|
175
|
+
Enable it fleet-wide via **Settings → "Live-checkout auto-freshen (fleet-wide)"**, or in `config.json`:
|
|
176
|
+
|
|
177
|
+
```jsonc
|
|
178
|
+
{ "engine": { "liveCheckoutAutoFreshen": true } } // fleet-wide
|
|
179
|
+
// or per-project (wins over fleet-wide):
|
|
180
|
+
{ "projects": [ { "name": "office", "checkoutMode": "live", "liveCheckoutAutoFreshen": true } ] }
|
|
181
|
+
```
|
|
182
|
+
|
|
156
183
|
|
|
157
184
|
### 7. Refuse on stale non-main base when forking a new branch (W-mr3lunnq000o9f41)
|
|
158
185
|
|
package/docs/runtime-adapters.md
CHANGED
|
@@ -82,10 +82,113 @@ real behavioral split appears between adapters.
|
|
|
82
82
|
| `resumeBookkeepingTurn` | ✓ | — | — | Claude CLI injects a synthetic "Continue from where you left off." meta turn on `--resume`; CC prompts must tell the model not to treat it as user intent. |
|
|
83
83
|
| `streamConsumer` | ✓ | ✓ | ✓ | Adapter implements `createStreamConsumer(ctx)` — required by `engine/llm.js` accumulator. |
|
|
84
84
|
| `imageInput` | ✓ | ✓ | ✗ | Runtime accepts `images: [{mimeType, dataBase64}]` and delivers them to the CLI. Claude: Anthropic Messages base64 content-block envelope via `--input-format stream-json`. Copilot: base64 payloads materialized to tmp files and passed as `--attachment <path>` (W-mqv7324u0021db5d). When false, `_resolveImageOpts` returns a typed `model-unavailable` error. |
|
|
85
|
+
| `acpWorkerPool` | ✗ | ✓ | ✗ | Runtime supports `<bin> --acp` (Agent Client Protocol), so it can be routed through a persistent worker pool instead of cold-spawning a fresh CLI process per turn/dispatch. Two independent consumers gate on this flag: `engine/cc-worker-pool.js` (CC/doc-chat tabs, `resolveCcUseWorkerPool`) and `engine/agent-worker-pool.js` (fleet agent dispatch, `resolveAgentUseWorkerPool`). Both resolvers hard-return `false` whenever this flag isn't `true` on the resolved runtime, regardless of any operator override, so a future non-ACP runtime can never be silently pooled. See [ACP Worker Pool](#acp-worker-pool-fleet-agent-dispatch) below. |
|
|
85
86
|
|
|
86
87
|
When a behavior is genuinely uniform across all current adapters, it still gets
|
|
87
88
|
a flag if a future runtime might disagree — flags are cheap.
|
|
88
89
|
|
|
90
|
+
## ACP Worker Pool (Fleet Agent Dispatch)
|
|
91
|
+
|
|
92
|
+
Copilot's `acpWorkerPool` capability backs **two** independent opt-in pools that
|
|
93
|
+
avoid cold-spawning a fresh `copilot` CLI process for every turn/dispatch. Both
|
|
94
|
+
share the same spawn + JSON-RPC transport (`engine/acp-transport.js`) but have
|
|
95
|
+
different ownership models:
|
|
96
|
+
|
|
97
|
+
| Pool | Module | Ownership model | Config | Default |
|
|
98
|
+
|------|--------|------------------|--------|---------|
|
|
99
|
+
| CC / doc-chat | `engine/cc-worker-pool.js` | 1 worker permanently owned per CC tab, kept warm for the tab's lifetime | `engine.ccUseWorkerPool` | `true` (Copilot CC) — see [docs/command-center.md](./command-center.md) |
|
|
100
|
+
| Fleet agent dispatch | `engine/agent-worker-pool.js` | 1 worker leased to exactly one **active** dispatch at a time, returned to a shared free set on release — not owned for the dispatch's whole life | `engine.agentUseWorkerPool` + `engine.agentAcpPoolSize` | `false` (opt-in) |
|
|
101
|
+
|
|
102
|
+
This section covers the fleet agent pool (P-9b3d5f61 / P-1d8f0b93 / P-4c6e8a72).
|
|
103
|
+
|
|
104
|
+
### Config keys
|
|
105
|
+
|
|
106
|
+
| Key | Resolver | Chain | Default |
|
|
107
|
+
|-----|----------|-------|---------|
|
|
108
|
+
| `engine.agentUseWorkerPool` | `shared.resolveAgentUseWorkerPool(engine, runtime)` | 1. Hard `false` unless `runtime.capabilities.acpWorkerPool === true` (Copilot only). 2. `engine.agentUseWorkerPool` explicit `true`/`false` override. 3. `ENGINE_DEFAULTS.agentUseWorkerPool`. | `false` |
|
|
109
|
+
| `engine.agentAcpPoolSize` | `shared.resolveAgentAcpPoolSize(engine)` | 1. `engine.agentAcpPoolSize` (positive number, else ignored). 2. `engine.maxConcurrent` (fleet dispatch concurrency cap). 3. `ENGINE_DEFAULTS.maxConcurrent`. | `ENGINE_DEFAULTS.maxConcurrent` (currently `5`) |
|
|
110
|
+
|
|
111
|
+
`agentUseWorkerPool` is deliberately **opt-in** (`false` by default), unlike
|
|
112
|
+
`ccUseWorkerPool`'s default-`true` — the fleet dispatch path has a much larger
|
|
113
|
+
MCP-auth-popup / isolation surface than the single CC singleton, so it stays
|
|
114
|
+
off until proven safe in a given operator's environment. `agentAcpPoolSize`
|
|
115
|
+
defaults to the same cap the engine would already use for cold-spawn
|
|
116
|
+
concurrency (`maxConcurrent`), so enabling the pool never queues more
|
|
117
|
+
concurrent dispatches than the fleet would have run anyway — it only changes
|
|
118
|
+
*how* those dispatches get a CLI process, not how many run at once.
|
|
119
|
+
|
|
120
|
+
### How dispatches route through the pool
|
|
121
|
+
|
|
122
|
+
When `resolveAgentUseWorkerPool` resolves `true`, `engine.js#spawnAgent` skips
|
|
123
|
+
`node engine/spawn-agent.js` and instead calls `engine/agent-worker-pool.js#acquireWorker`,
|
|
124
|
+
wrapping the lease in `engine/pooled-agent-process.js#PooledAgentProcess` — a
|
|
125
|
+
child_process-shaped facade (`.pid`, `.stdout`/`.stderr`, `.kill()`,
|
|
126
|
+
`'close'`/`'error'`) so the rest of `spawnAgent`'s downstream code (stdout/stderr
|
|
127
|
+
handlers, PID-file writing, live-output.log, timeout/steering cancel-vs-kill,
|
|
128
|
+
restart-reattach) runs unmodified regardless of whether `proc` is a real OS
|
|
129
|
+
process or a pooled lease. Assignment strategy, in order:
|
|
130
|
+
|
|
131
|
+
1. A **free** worker whose MCP-server set already matches wins first — reused
|
|
132
|
+
via a fresh `session/new`, no respawn.
|
|
133
|
+
2. Otherwise another free worker is **evicted** (closed) and a replacement is
|
|
134
|
+
spawned fresh with the requested MCP servers/cwd (MCP servers are resolved
|
|
135
|
+
at `copilot --acp` process boot, so a changed server set requires a full
|
|
136
|
+
respawn, not just a new session).
|
|
137
|
+
3. Otherwise, if the pool has room (fewer than `agentAcpPoolSize` workers
|
|
138
|
+
allocated across free + busy + in-flight-spawn), a brand-new worker spawns.
|
|
139
|
+
4. Otherwise the request queues FIFO until a `release()` or worker crash frees
|
|
140
|
+
a slot.
|
|
141
|
+
|
|
142
|
+
Dispatch records persist `item.pooled = true` on disk (`dispatch.json`), not
|
|
143
|
+
just the in-memory `activeProcesses._pooled` flag (which is wiped on engine
|
|
144
|
+
restart) — the on-disk flag is what tells the restart-reattach path
|
|
145
|
+
(`engine/cli.js`) that a live worker PID is a pooled lease, not a resumable
|
|
146
|
+
cold-spawned child process.
|
|
147
|
+
|
|
148
|
+
### Isolation tradeoff — read this before enabling `agentUseWorkerPool`
|
|
149
|
+
|
|
150
|
+
**Enabling the pool bounds the MCP-auth-popup / cross-dispatch isolation risk
|
|
151
|
+
by pool size — it does not eliminate it.** Cold-spawn gives every dispatch its
|
|
152
|
+
own fresh OS process and its own `COPILOT_HOME`; the pool instead keeps up to
|
|
153
|
+
`agentAcpPoolSize` `copilot --acp` processes warm and reuses them across
|
|
154
|
+
dispatches whenever the MCP-server set matches. Concretely:
|
|
155
|
+
|
|
156
|
+
- A worker process — and anything an MCP server inside it authenticated,
|
|
157
|
+
cached, or leaked into process memory during a prior dispatch — can be
|
|
158
|
+
handed to a **different** dispatch/work item next, as long as the MCP-server
|
|
159
|
+
set is unchanged. There is no per-dispatch process boundary the way
|
|
160
|
+
cold-spawn provides.
|
|
161
|
+
- The blast radius of a compromised or misbehaving MCP server is capped at
|
|
162
|
+
`agentAcpPoolSize` concurrently-warm workers, not the whole fleet — but it is
|
|
163
|
+
**not zero**. A larger pool size widens the number of dispatches that could
|
|
164
|
+
share exposure to the same warm worker over its lifetime; a smaller pool
|
|
165
|
+
size narrows it at the cost of more evictions/respawns (and therefore more
|
|
166
|
+
MCP-auth prompts) when the requested MCP-server set changes between
|
|
167
|
+
dispatches.
|
|
168
|
+
- This mirrors the same tradeoff `engine.ccUseWorkerPool` already accepts for
|
|
169
|
+
CC tabs (see [docs/command-center.md](./command-center.md)), which is why
|
|
170
|
+
`agentUseWorkerPool` defaults `false`: the fleet dispatch surface has more,
|
|
171
|
+
and more varied, work items than a single CC session, so the exposure window
|
|
172
|
+
per warm worker is larger and the default stays conservative until an
|
|
173
|
+
operator has evaluated the tradeoff for their own MCP-server set.
|
|
174
|
+
|
|
175
|
+
Operators enabling `engine.agentUseWorkerPool` should choose `agentAcpPoolSize`
|
|
176
|
+
deliberately: a pool size of `1` gives the strongest isolation (still weaker
|
|
177
|
+
than cold-spawn, since consecutive dispatches on that single worker still
|
|
178
|
+
share process state) at the cost of maximum serialization/respawns; a larger
|
|
179
|
+
pool size trades some isolation for parallelism and fewer respawns.
|
|
180
|
+
|
|
181
|
+
### See Also (ACP pool)
|
|
182
|
+
|
|
183
|
+
- `engine/agent-worker-pool.js` — pool mechanics, assignment strategy, JSDoc header.
|
|
184
|
+
- `engine/pooled-agent-process.js` — the child_process-shaped facade and its
|
|
185
|
+
documented output-translation scope limits (no tool-call/model-announcement
|
|
186
|
+
event synthesis — a deliberate parity gap, not an oversight).
|
|
187
|
+
- `test/unit/agent-worker-pool.test.js`, `test/integration/agent-pool-dispatch.test.js`,
|
|
188
|
+
`test/unit/pooled-cold-parity-matrix.test.js`, `test/unit/pooled-cancel-vs-kill.test.js`,
|
|
189
|
+
`test/unit/pooled-dispatch-restart-reattach.test.js` — pool mechanics, end-to-end
|
|
190
|
+
flow, cold/pooled parity matrix, cancel-vs-kill, and restart-reattach coverage.
|
|
191
|
+
|
|
89
192
|
## CC vs Agent Runtime Independence
|
|
90
193
|
|
|
91
194
|
Command Center (CC) / doc-chat and the agent fleet are two **independent**
|