wicked-crew 0.15.8 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/chat-attachments.d.ts +102 -0
- package/dist/api/chat-attachments.d.ts.map +1 -0
- package/dist/api/chat-attachments.js +438 -0
- package/dist/api/chat-attachments.js.map +1 -0
- package/dist/api/chat-history.d.ts +90 -0
- package/dist/api/chat-history.d.ts.map +1 -0
- package/dist/api/chat-history.js +279 -0
- package/dist/api/chat-history.js.map +1 -0
- package/dist/api/chat-transcripts.d.ts +4 -2
- package/dist/api/chat-transcripts.d.ts.map +1 -1
- package/dist/api/chat-transcripts.js +13 -2
- package/dist/api/chat-transcripts.js.map +1 -1
- package/dist/api/roster-standing.d.ts.map +1 -1
- package/dist/api/roster-standing.js +8 -1
- package/dist/api/roster-standing.js.map +1 -1
- package/dist/api/routes.d.ts +39 -0
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +478 -36
- package/dist/api/routes.js.map +1 -1
- package/dist/api/seat-agents.d.ts +11 -3
- package/dist/api/seat-agents.d.ts.map +1 -1
- package/dist/api/seat-agents.js +2 -0
- package/dist/api/seat-agents.js.map +1 -1
- package/dist/api/seat-bench-trail.d.ts +3 -1
- package/dist/api/seat-bench-trail.d.ts.map +1 -1
- package/dist/api/seat-bench-trail.js +27 -9
- package/dist/api/seat-bench-trail.js.map +1 -1
- package/dist/api/seat-health.d.ts +56 -1
- package/dist/api/seat-health.d.ts.map +1 -1
- package/dist/api/seat-health.js +147 -19
- package/dist/api/seat-health.js.map +1 -1
- package/dist/api/seat-standing.d.ts +11 -0
- package/dist/api/seat-standing.d.ts.map +1 -1
- package/dist/api/seat-standing.js +13 -6
- package/dist/api/seat-standing.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +18 -0
- package/dist/api/server.js.map +1 -1
- package/dist/api/stall-watchdog.d.ts +10 -0
- package/dist/api/stall-watchdog.d.ts.map +1 -1
- package/dist/api/stall-watchdog.js +12 -0
- package/dist/api/stall-watchdog.js.map +1 -1
- package/dist/api/system-snapshot.d.ts +382 -0
- package/dist/api/system-snapshot.d.ts.map +1 -0
- package/dist/api/system-snapshot.js +1164 -0
- package/dist/api/system-snapshot.js.map +1 -0
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +5 -3
- package/dist/core/deliver.js.map +1 -1
- package/dist/editors/registry.d.ts +20 -20
- package/dist/studio/assets/{index-Bhwz-oBW.js → index-BnnXv6Ij.js} +242 -226
- package/dist/studio/assets/index-Ce_RMQzq.css +32 -0
- package/dist/studio/editors/wicked-deck/editor.json +1 -1
- package/dist/studio/editors/wicked-deck/index.html +1 -1
- package/dist/studio/editors/wicked-doc/editor.json +1 -1
- package/dist/studio/editors/wicked-doc/index.html +1 -1
- package/dist/studio/editors/wicked-page/editor.json +1 -1
- package/dist/studio/editors/wicked-page/index.html +1 -1
- package/dist/studio/editors/wicked-video/editor.json +1 -1
- package/dist/studio/editors/wicked-video/index.html +1 -1
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +569 -88
- package/endpoint-manifest.json +47 -1
- package/package.json +3 -3
- package/dist/studio/assets/index-CmNUtpfw.css +0 -32
package/dist/api/routes.js
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
import { listRequirements, getRequirement, patchRequirement } from './requirements.js';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { promises as fsp } from 'node:fs';
|
|
10
10
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
@@ -13,6 +13,11 @@ import { videoPalette } from '../appearance/video-palette.js';
|
|
|
13
13
|
import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, GovernanceReplayUnsupportedError, PlanLaunchUnsupportedError, SteeringUnsupportedError, addonSupportsQeOverride, addonSupportsReducedAssurance, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
|
|
14
14
|
import { codeGraphDb, codeGraphErrorStatus, requirementsGraph } from '../core/repoPaths.js';
|
|
15
15
|
import { resolveCursorUnit } from '../core/cursor.js';
|
|
16
|
+
/** Create `dir` (private) when missing and answer it — a read root must exist when a run launches. */
|
|
17
|
+
function ensureDir(dir) {
|
|
18
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
16
21
|
import { detectRefusal, isOpenGate, offeredChoices, replayOpenGate } from './gate-cache.js';
|
|
17
22
|
import { acceptanceRequirementOf, buildAcceptanceView } from '../qe/acceptance.js';
|
|
18
23
|
import { runIdentityOf, wireIdentity } from '../core/run-identity.js';
|
|
@@ -28,7 +33,7 @@ import { chatSeatAdmission } from './seat-standing.js';
|
|
|
28
33
|
import { seatGovernanceDiagnostics, seatGovernanceWarnings, } from './seat-governance.js';
|
|
29
34
|
import { seatWorkDefaultNow } from './seat-work-default.js';
|
|
30
35
|
import { ROSTER_REFRESH_WAIT_MS, rosterWithStandingFactory } from './roster-standing.js';
|
|
31
|
-
import { SeatContainerStatus, isContainerLoginCli, isContainerSeatFloor, loginDetail, runSeatLogin, seatLoginDepsFor, } from './seat-container-login.js';
|
|
36
|
+
import { SeatContainerStatus, gatewayDirOf, isContainerLoginCli, isContainerSeatFloor, isHostSeatFloor, loginDetail, runSeatLogin, seatLoginDepsFor, } from './seat-container-login.js';
|
|
32
37
|
import { ChatTurnIndex } from './chat-turns.js';
|
|
33
38
|
import { ChatScopeIndex, chatScopeDeps, chatScopeStatement, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
|
|
34
39
|
import { ASK_TURN_BUDGET_SECS, AskPathIndex, answerStep, isAskTurn } from './ask-paths.js';
|
|
@@ -69,6 +74,7 @@ import { aggregateReceipt, postHocDeliveryAssurance, qeAcceptanceFromView, qeAcc
|
|
|
69
74
|
import { ProjectSettingsStore } from '../projects/settings.js';
|
|
70
75
|
import { isHumanOperator } from './watch-routes.js';
|
|
71
76
|
import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
|
|
77
|
+
import { defaultInteractiveRoot } from '../interactive/bridge-root.js';
|
|
72
78
|
import { composeDeliverText, configuredPublicOrigin, extractFollowUps, factsFromRun, reportsFromOutputs, FOLLOW_UPS_MAX, framedDeliverText, runUrlFor, } from '../core/deliver-text.js';
|
|
73
79
|
import { deliverIdentityFor, deliverRepoFor, deliverTargetView, isGitHubLogin, pushIdentityOf, readDeliverOriginUrl, resolvePullRequest as resolvePullRequestViaGh, unverifiedTreesFrom, } from '../core/deliver.js';
|
|
74
80
|
import { credentialsMissingIn, deliverCredentialsProbe, missingCredentials } from '../core/deliver-credentials.js';
|
|
@@ -76,7 +82,7 @@ import { codebaseArchiveUrl } from './codebase-archive.js';
|
|
|
76
82
|
import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
|
|
77
83
|
import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
|
|
78
84
|
import { registerInteractiveDocChecks } from '../interactive/doc-checks-routes.js';
|
|
79
|
-
import { crewStateHome } from '../projects/state-home.js';
|
|
85
|
+
import { crewStateHome, stateHomeOfDb } from '../projects/state-home.js';
|
|
80
86
|
import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
|
|
81
87
|
import { REVIEWS_DIRNAME } from '../interactive/review-ledger.js';
|
|
82
88
|
import { MembershipIndex } from '../projects/membership-index.js';
|
|
@@ -85,6 +91,9 @@ import { resolveLinkedIssues } from '../core/linked-issues.js';
|
|
|
85
91
|
import { AuditLog } from './audit.js';
|
|
86
92
|
import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion, hostSandboxLauncher, seatRosterFile, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
|
|
87
93
|
import { GovernanceDiagnostics } from './governance-health.js';
|
|
94
|
+
import { ATTACHMENT_LIMITS, ATTACHMENT_MAX_MESSAGE_BYTES, AttachmentError, ChatAttachmentStore, attachmentWire, attachmentsBlock, boundaryOf, parseMultipart, stageForSeat } from './chat-attachments.js';
|
|
95
|
+
import { CHAT_HISTORY_MAX_LIMIT, ChatHistoryStore, decodeCursor } from './chat-history.js';
|
|
96
|
+
import { PROBE_TIMEOUT_MS, SYSTEM_BUDGET_MS, SYSTEM_STATS_DAYS, WeekFoldCache, assembleSnapshot, buildAttention, buildClis, buildGovernance, diskRow, estateReadonlyCall, infraRow, judgeGaps, livenessThresholds, loadRow, probeBroker, probeBus, probeEstate, probeGateway, probeInteractive, probeVault, readBridgeLockAsync, readWeek, resolveContainerRuntime, runLiveness, settleWithin, skillsRow, sortRuns, stateHomeRow, unfinished, } from './system-snapshot.js';
|
|
88
97
|
import { governanceStoreFix, legacyHomeOutboxPath } from '../core/governance-store.js';
|
|
89
98
|
import { UsageError, replayOutboxInto } from '../cli/governance.js';
|
|
90
99
|
import { foldDeadletters } from './governance-health.js';
|
|
@@ -697,6 +706,8 @@ export const ChatOpenSchema = z.object({
|
|
|
697
706
|
scopeKind: z.enum(['system', 'everything', 'project', 'repo', 'repos', 'none']).optional(),
|
|
698
707
|
/** (ASK-C1) The seat the operator chose as the primary agent; omitted ⇒ the engine picks at random. */
|
|
699
708
|
primary: z.string().min(1).optional(),
|
|
709
|
+
/** (api-types 0.137.0, DES-SYSTEM-CHAT) The page the chat was started on, for the history list. */
|
|
710
|
+
startedOn: z.object({ route: z.string().min(1).max(512), label: z.string().min(1).max(200) }).strict().optional(),
|
|
700
711
|
}).strict();
|
|
701
712
|
/**
|
|
702
713
|
* The `singleSeat` disclosure (crew#641, hole closed in crew#650) — built in ONE place so the 201
|
|
@@ -802,6 +813,8 @@ runtime = {}) {
|
|
|
802
813
|
const chatTurns = runtime.chatTurns ?? new ChatTurnIndex();
|
|
803
814
|
const askPaths = runtime.askPaths ?? new AskPathIndex();
|
|
804
815
|
const chatTranscripts = runtime.chatTranscripts;
|
|
816
|
+
const chatAttachments = runtime.chatAttachments ?? new ChatAttachmentStore();
|
|
817
|
+
const chatHistory = runtime.chatHistory ?? (chatTranscripts !== undefined ? new ChatHistoryStore(chatTranscripts, chatAttachments) : null);
|
|
805
818
|
const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
|
|
806
819
|
// Wave 6: the doc↔run binding and the registered test sets — `createServer` injects the real
|
|
807
820
|
// ones; a directly-driven route set gets an empty index (every run `document_id: null`, no sets).
|
|
@@ -1176,6 +1189,27 @@ runtime = {}) {
|
|
|
1176
1189
|
// The daemon's QE acceptance setting as read now ('required' when the settings cannot be read:
|
|
1177
1190
|
// fail closed, never off by accident).
|
|
1178
1191
|
const qeAcceptanceNow = async () => qeAcceptanceMode(await Promise.resolve().then(() => adapter.getSettings()).catch(() => null));
|
|
1192
|
+
// The `/health.warnings` list, shared with `GET /system`'s daemon row (DES-SYSTEM-VIEW).
|
|
1193
|
+
const healthWarningsOf = (stateHome, baseSkill) => [
|
|
1194
|
+
...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
|
|
1195
|
+
...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
|
|
1196
|
+
// crew#753: a garden older than the minimum is a blocking finding on the first screen too.
|
|
1197
|
+
...[gardenBlocker()].filter((f) => f !== null),
|
|
1198
|
+
// DES-TEAMING-002 T0: the daemon's bus did not open at boot, so the engine was handed none
|
|
1199
|
+
// and runs un-teamed. A daemon-level notice (status stays ok: it serves); the per-run
|
|
1200
|
+
// `transport:"none"` record lands with P1.
|
|
1201
|
+
...(adapter.busUnavailable != null ? [busUnavailableWarning(adapter.busUnavailable)] : []),
|
|
1202
|
+
// wicked-core#631: the engine has the bus but not the calls crew reaches it through.
|
|
1203
|
+
...(adapter.busSeamsOff != null ? [busSeamsOffWarning(adapter.busSeamsOff)] : []),
|
|
1204
|
+
// crew#742: a seat that runs ungoverned, or floor seats on a host that cannot arm their
|
|
1205
|
+
// boundary, are named before any run starts.
|
|
1206
|
+
...seatGovernanceWarnings(seatGovernanceNow(), hostSandboxLauncher()),
|
|
1207
|
+
// crew 0.15.2 (#916): a custom seat written council-off with no work flag is now work-eligible
|
|
1208
|
+
// by the engine default; each is named once, so the operator can keep it off work.
|
|
1209
|
+
...seatWorkDefaultNow(),
|
|
1210
|
+
// crew#1068: the engine has no wicked-vault CLI to record held boundary findings with.
|
|
1211
|
+
...[adapter.vaultBin != null ? vaultBinWarning(adapter.vaultBin) : null].filter((w) => w !== null),
|
|
1212
|
+
];
|
|
1179
1213
|
app.get(`${V}/health`, { config: { manifest: { statusCodes: [200] } } }, async () => {
|
|
1180
1214
|
const ping = await enginePing();
|
|
1181
1215
|
// F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
|
|
@@ -1207,6 +1241,11 @@ runtime = {}) {
|
|
|
1207
1241
|
qeAcceptance: await qeAcceptanceNow(),
|
|
1208
1242
|
// api-types 0.128.0: `LaunchRunBody.primary` (the lead agent) is accepted.
|
|
1209
1243
|
launchPrimary: true,
|
|
1244
|
+
// api-types 0.136.0 (DES-SYSTEM-VIEW): `GET /system`, the Nerve Center snapshot, is served.
|
|
1245
|
+
system: true,
|
|
1246
|
+
// api-types 0.137.0 (DES-SYSTEM-CHAT): the chat history (list, reopen, close) and attachments.
|
|
1247
|
+
chatList: chatHistory !== null,
|
|
1248
|
+
chatAttachments: true,
|
|
1210
1249
|
};
|
|
1211
1250
|
// wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
|
|
1212
1251
|
// daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
|
|
@@ -1223,26 +1262,7 @@ runtime = {}) {
|
|
|
1223
1262
|
// `baseSkill.finding` and `/diagnostics.skills.findings[]` (one surface, no new field;
|
|
1224
1263
|
// `HealthWarning.kind` is open); status stays ok because the daemon serves and studio must load
|
|
1225
1264
|
// and show it — exactly the state-home blocker's precedent above.
|
|
1226
|
-
const warnings =
|
|
1227
|
-
...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
|
|
1228
|
-
...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
|
|
1229
|
-
// crew#753: a garden older than the minimum is a blocking finding on the first screen too.
|
|
1230
|
-
...[gardenBlocker()].filter((f) => f !== null),
|
|
1231
|
-
// DES-TEAMING-002 T0: the daemon's bus did not open at boot, so the engine was handed none
|
|
1232
|
-
// and runs un-teamed. A daemon-level notice (status stays ok: it serves); the per-run
|
|
1233
|
-
// `transport:"none"` record lands with P1.
|
|
1234
|
-
...(adapter.busUnavailable != null ? [busUnavailableWarning(adapter.busUnavailable)] : []),
|
|
1235
|
-
// wicked-core#631: the engine has the bus but not the calls crew reaches it through.
|
|
1236
|
-
...(adapter.busSeamsOff != null ? [busSeamsOffWarning(adapter.busSeamsOff)] : []),
|
|
1237
|
-
// crew#742: a seat that runs ungoverned, or floor seats on a host that cannot arm their
|
|
1238
|
-
// boundary, are named before any run starts.
|
|
1239
|
-
...seatGovernanceWarnings(seatGovernanceNow(), hostSandboxLauncher()),
|
|
1240
|
-
// crew 0.15.2 (#916): a custom seat written council-off with no work flag is now work-eligible
|
|
1241
|
-
// by the engine default; each is named once, so the operator can keep it off work.
|
|
1242
|
-
...seatWorkDefaultNow(),
|
|
1243
|
-
// crew#1068: the engine has no wicked-vault CLI to record held boundary findings with.
|
|
1244
|
-
...[adapter.vaultBin != null ? vaultBinWarning(adapter.vaultBin) : null].filter((w) => w !== null),
|
|
1245
|
-
];
|
|
1265
|
+
const warnings = healthWarningsOf(stateHome, baseSkill);
|
|
1246
1266
|
return {
|
|
1247
1267
|
status: 'ok',
|
|
1248
1268
|
version: PKG_VERSION,
|
|
@@ -1265,7 +1285,9 @@ runtime = {}) {
|
|
|
1265
1285
|
// binding when the addon has one, and the dead-letter outbox folded on change. Built at boot so
|
|
1266
1286
|
// the record baseline is the boot baseline.
|
|
1267
1287
|
const governanceDiagnostics = new GovernanceDiagnostics(adapter.governanceStore ?? null, CoreAdapter.eventStoreCounter(), runtime.governanceLegacyOutboxPath === undefined ? legacyHomeOutboxPath() : runtime.governanceLegacyOutboxPath);
|
|
1268
|
-
|
|
1288
|
+
// The diagnostics body, shared by `GET /diagnostics` (the raw dump) and `GET /system` (the
|
|
1289
|
+
// Nerve Center snapshot, which carries every block of it on its rows — DES-SYSTEM-VIEW).
|
|
1290
|
+
const diagnosticsNow = async () => {
|
|
1269
1291
|
// `dbPath` is a readonly field of the real adapter; a stub-driven route set may lack it,
|
|
1270
1292
|
// and diagnostics over an unknown store honestly reports no stores and no ACP record.
|
|
1271
1293
|
const dbPath = typeof adapter.dbPath === 'string' && adapter.dbPath !== '' ? adapter.dbPath : null;
|
|
@@ -1323,7 +1345,8 @@ runtime = {}) {
|
|
|
1323
1345
|
// `null` on a route set driven without the real adapter.
|
|
1324
1346
|
vault: adapter.vaultBin != null ? vaultDiagnostics(adapter.vaultBin) : null,
|
|
1325
1347
|
};
|
|
1326
|
-
}
|
|
1348
|
+
};
|
|
1349
|
+
app.get(`${V}/diagnostics`, { config: { manifest: { responseType: 'DiagnosticsResponse', statusCodes: [200] } } }, async () => diagnosticsNow());
|
|
1327
1350
|
// Who am I talking to the daemon as? (task #88). In local mode this is
|
|
1328
1351
|
// always the implicit full-trust local actor; in required mode it names the
|
|
1329
1352
|
// token's actor — the cheap probe a skin uses to decide what to render.
|
|
@@ -3070,6 +3093,17 @@ runtime = {}) {
|
|
|
3070
3093
|
// DC-S7: only a chat that actually OPENED is preface-eligible — recorded here, after the scope
|
|
3071
3094
|
// is published, so a refused open leaves no per-chat state behind (codex r1).
|
|
3072
3095
|
considerations.noteChatOpen(chatId, inForceAtOpen);
|
|
3096
|
+
// DES-SYSTEM-CHAT: the history entry (kept after the chat ends; only DELETE removes it).
|
|
3097
|
+
try {
|
|
3098
|
+
chatHistory?.opened(chatId, {
|
|
3099
|
+
startedOn: b.startedOn,
|
|
3100
|
+
cli: b.primary ?? clis[0] ?? null,
|
|
3101
|
+
actorId: security.authMode === 'required' ? actorOf(req).id : null,
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
catch (err) {
|
|
3105
|
+
req.log.warn({ chatId }, `chat history entry not written: ${message(err)}`);
|
|
3106
|
+
}
|
|
3073
3107
|
// The thread learns of every refused seat the way it learns of everything else — a frame
|
|
3074
3108
|
// on /ws — AFTER the scope is published, so a reader never sees a refusal for a chat it
|
|
3075
3109
|
// cannot yet look up.
|
|
@@ -3127,6 +3161,8 @@ runtime = {}) {
|
|
|
3127
3161
|
const ChatMessageSchema = z.object({
|
|
3128
3162
|
text: z.string().min(1).max(65536),
|
|
3129
3163
|
targets: z.array(z.string().min(1)).min(1).max(8).optional(),
|
|
3164
|
+
/** (api-types 0.137.0) Attachment ids from `POST /chats/:id/attachments`. */
|
|
3165
|
+
attachments: z.array(z.string().min(1).max(64)).optional(),
|
|
3130
3166
|
}).strict();
|
|
3131
3167
|
// F-RECON-017: a message sent while a targeted seat is still answering the previous one is
|
|
3132
3168
|
// REFUSED, not queued. The engine queues silently (the worker gets the text the instant the
|
|
@@ -3156,6 +3192,21 @@ runtime = {}) {
|
|
|
3156
3192
|
return reply.code(409).send({ error: `chat '${id}' has no eligible seat — sign a seat in and re-seat it (POST /chats/:id/seats)` });
|
|
3157
3193
|
}
|
|
3158
3194
|
const text = parsed.data.text;
|
|
3195
|
+
// DES-SYSTEM-CHAT: the attachments this message carries — every id this chat's, within the caps,
|
|
3196
|
+
// refused before anything is reserved or sent.
|
|
3197
|
+
let attached = [];
|
|
3198
|
+
if (parsed.data.attachments !== undefined && parsed.data.attachments.length > 0) {
|
|
3199
|
+
try {
|
|
3200
|
+
attached = chatAttachments.resolve(id, parsed.data.attachments);
|
|
3201
|
+
}
|
|
3202
|
+
catch (err) {
|
|
3203
|
+
if (err instanceof AttachmentError)
|
|
3204
|
+
return reply.code(err.status).send(err.body());
|
|
3205
|
+
throw err;
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
// What the seats receive: the operator's words, then the attachments block (paths + text).
|
|
3209
|
+
let seatText = text;
|
|
3159
3210
|
let scope = chatScopes.get(id);
|
|
3160
3211
|
const storedScope = scope;
|
|
3161
3212
|
let engine = chatScopes.engineOf(id);
|
|
@@ -3195,7 +3246,7 @@ runtime = {}) {
|
|
|
3195
3246
|
after();
|
|
3196
3247
|
if (preface !== null)
|
|
3197
3248
|
chatTranscripts?.appendSystem(id, turnId, preface);
|
|
3198
|
-
chatTranscripts?.appendUser(id, turnId, text, voice);
|
|
3249
|
+
chatTranscripts?.appendUser(id, turnId, text, voice, attached.map(attachmentWire));
|
|
3199
3250
|
// The recorder's audience is the turn's AS THE TURN INDEX HOLDS IT NOW: the eligible roster
|
|
3200
3251
|
// for a random pick, or the PA the relay narrowed it to during the engine awaits above
|
|
3201
3252
|
// (`path.started` / `path.repicked` may land before this record exists — ASK-C2, codex on
|
|
@@ -3213,7 +3264,7 @@ runtime = {}) {
|
|
|
3213
3264
|
};
|
|
3214
3265
|
// DC-S7: the rules the chat's seats were told about ride the step's instructions, as the
|
|
3215
3266
|
// preface rode a pool send; told only once the send is accepted (`commit`).
|
|
3216
|
-
const withPreface = (preface) => (preface !== null ? `${preface}\n\n${
|
|
3267
|
+
const withPreface = (preface) => (preface !== null ? `${preface}\n\n${seatText}` : seatText);
|
|
3217
3268
|
try {
|
|
3218
3269
|
const rules = (await considerations.inForceFor(scope?.projectId ?? null)).rules;
|
|
3219
3270
|
// crew#1020: a project/everything chat must see repos attached since open. Admission was
|
|
@@ -3297,6 +3348,22 @@ runtime = {}) {
|
|
|
3297
3348
|
chatTranscripts?.registerRoots(id, scope.repos.map((r) => ({ absRoot: resolve(r.rootPath), name: r.name })));
|
|
3298
3349
|
}
|
|
3299
3350
|
}
|
|
3351
|
+
// DES-SYSTEM-CHAT: stage the attachments read-only in the chat's scratch root (a read root of
|
|
3352
|
+
// its run since launch) and list them in the turn's instructions.
|
|
3353
|
+
const attachRoot = scope !== undefined ? join(scope.cwd, 'attachments') : null;
|
|
3354
|
+
if (attached.length > 0) {
|
|
3355
|
+
if (attachRoot === null) {
|
|
3356
|
+
askPaths.release(id, lease);
|
|
3357
|
+
return reply.code(409).send({ error: `chat '${id}' has no scratch root to hand its attachments to the seat; nothing was sent` });
|
|
3358
|
+
}
|
|
3359
|
+
try {
|
|
3360
|
+
seatText = `${text}\n${attachmentsBlock(stageForSeat(attached, attachRoot))}`;
|
|
3361
|
+
}
|
|
3362
|
+
catch (err) {
|
|
3363
|
+
askPaths.release(id, lease);
|
|
3364
|
+
return reply.code(500).send({ error: `could not stage the attachments for the seat: ${message(err)}` });
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3300
3367
|
voice = path.pa !== null ? [path.pa] : [];
|
|
3301
3368
|
// A random pick reserves every eligible seat until the relay names the PA.
|
|
3302
3369
|
audience = voice.length > 0 ? voice : path.eligible;
|
|
@@ -3324,10 +3391,15 @@ runtime = {}) {
|
|
|
3324
3391
|
...(scope?.kind === 'project' && codeGraphDb !== null ? { projectGraph: { dbPath: codeGraphDb } } : {}),
|
|
3325
3392
|
// crew#1034: the scope's repositories are the run's read roots, graph or no graph — the
|
|
3326
3393
|
// statement tells the seats to read them at their paths, so the boundary must let them.
|
|
3327
|
-
|
|
3394
|
+
// DES-SYSTEM-CHAT: plus the chat's attachments directory, from launch (a run cannot gain a
|
|
3395
|
+
// read root later), so a file attached on any later turn is readable too.
|
|
3396
|
+
...(readRoots.length > 0 || attachRoot !== null
|
|
3397
|
+
? { extraReadRoots: [...readRoots, ...(attachRoot !== null ? [ensureDir(attachRoot)] : [])] }
|
|
3398
|
+
: {}),
|
|
3328
3399
|
};
|
|
3329
3400
|
const runId = await adapter.launchRun(input);
|
|
3330
|
-
|
|
3401
|
+
// The scope's roots (not the attachments directory) are what a later turn compares against.
|
|
3402
|
+
if (!askPaths.started(id, generation, runId, stepId, input.projectGraph?.dbPath ?? null, readRoots)) {
|
|
3331
3403
|
// The chat was closed (End) while the launch was in flight: the run must not outlive it.
|
|
3332
3404
|
await adapter.cancelRun(runId).catch(() => undefined);
|
|
3333
3405
|
if (turn !== null)
|
|
@@ -3488,7 +3560,32 @@ runtime = {}) {
|
|
|
3488
3560
|
// lived in the tab that abandoned it, and an operator could not reclaim one without restarting
|
|
3489
3561
|
// the daemon. Registered BEFORE `/chats/:id` is irrelevant to fastify (it routes on the literal
|
|
3490
3562
|
// segment first), but the order reads the way the routes nest.
|
|
3491
|
-
app.get(`${V}/chats`, { config: { manifest: { responseType: 'ChatListResponse', statusCodes: [200, 400, 501] } } }, async (
|
|
3563
|
+
app.get(`${V}/chats`, { config: { manifest: { responseType: 'ChatListResponse', statusCodes: [200, 400, 501] } } }, async (req, reply) => {
|
|
3564
|
+
// DES-SYSTEM-CHAT (api-types 0.137.0): the paged history rides beside the live rows.
|
|
3565
|
+
const q = req.query;
|
|
3566
|
+
const one = (v) => (Array.isArray(v) ? v[0] : v);
|
|
3567
|
+
const limitRaw = one(q.limit);
|
|
3568
|
+
const limit = limitRaw === undefined ? undefined : Number(limitRaw);
|
|
3569
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > CHAT_HISTORY_MAX_LIMIT)) {
|
|
3570
|
+
return reply.code(400).send({ error: `\`limit\` must be a whole number from 1 to ${CHAT_HISTORY_MAX_LIMIT}` });
|
|
3571
|
+
}
|
|
3572
|
+
const cursor = one(q.cursor);
|
|
3573
|
+
if (cursor !== undefined && decodeCursor(cursor) === null)
|
|
3574
|
+
return reply.code(400).send({ error: '`cursor` is not a cursor this daemon issued' });
|
|
3575
|
+
const status = one(q.status) ?? 'all';
|
|
3576
|
+
if (status !== 'all' && status !== 'open' && status !== 'closed') {
|
|
3577
|
+
return reply.code(400).send({ error: '`status` must be open, closed or all' });
|
|
3578
|
+
}
|
|
3579
|
+
const history = chatHistory === null
|
|
3580
|
+
? {}
|
|
3581
|
+
: chatHistory.list({
|
|
3582
|
+
...(limit !== undefined ? { limit } : {}),
|
|
3583
|
+
cursor: cursor ?? null,
|
|
3584
|
+
status,
|
|
3585
|
+
actorId: security.authMode === 'required' ? actorOf(req).id : null,
|
|
3586
|
+
liveCli: (cid) => askPaths.get(cid)?.pa ?? null,
|
|
3587
|
+
isLive: (cid) => askPaths.get(cid) !== undefined || chatScopes.get(cid) !== undefined,
|
|
3588
|
+
});
|
|
3492
3589
|
try {
|
|
3493
3590
|
const pool = await adapter.chatList().catch((err) => {
|
|
3494
3591
|
if (err instanceof ChatUnsupportedError)
|
|
@@ -3507,7 +3604,11 @@ runtime = {}) {
|
|
|
3507
3604
|
};
|
|
3508
3605
|
});
|
|
3509
3606
|
const seen = new Set(paths.map((p) => p.chatId));
|
|
3510
|
-
|
|
3607
|
+
// Under `auth: required` a live chat another actor opened is not listed (its history entry
|
|
3608
|
+
// names its opener); local mode lists every live chat, as before.
|
|
3609
|
+
const viewer = security.authMode === 'required' ? actorOf(req).id : null;
|
|
3610
|
+
const mine = (cid) => viewer === null || chatHistory === null || chatHistory.entry(cid) === null || chatHistory.visibleTo(cid, viewer);
|
|
3611
|
+
return { chats: [...paths, ...pool.filter((c) => !seen.has(c.chatId))].filter((c) => mine(c.chatId)), ...history };
|
|
3511
3612
|
}
|
|
3512
3613
|
catch (err) {
|
|
3513
3614
|
// A build that cannot do chat is a capability gap, not a bad request: 501 tells an operator to
|
|
@@ -3562,8 +3663,15 @@ runtime = {}) {
|
|
|
3562
3663
|
return reply.code(400).send({ error: message(err) });
|
|
3563
3664
|
}
|
|
3564
3665
|
});
|
|
3565
|
-
|
|
3566
|
-
|
|
3666
|
+
// Ending a chat — shared by DELETE (end + delete its history) and POST /close (end, keep it).
|
|
3667
|
+
const endChat = async (id,
|
|
3668
|
+
/** Runs right after the close fold, with NO await before it: the id cannot have been reopened
|
|
3669
|
+
* yet, so a history operation here can never land on a replacement chat (xreview r1 on #1140). */
|
|
3670
|
+
afterFold) => {
|
|
3671
|
+
// DES-SYSTEM-CHAT: the answering CLI the history names is the path's PA, while it is known.
|
|
3672
|
+
const pa = askPaths.get(id)?.pa;
|
|
3673
|
+
if (pa !== undefined && pa !== null)
|
|
3674
|
+
chatHistory?.setCli(id, pa);
|
|
3567
3675
|
// The scratch root goes with the chat at once, whether or not the engine still knew it
|
|
3568
3676
|
// (crew#502). ASK-C1: the daemon owns the close, so the id is HELD — no grace timer — until
|
|
3569
3677
|
// this close settles below; a reopen meanwhile is refused and a second DELETE joins this one
|
|
@@ -3597,17 +3705,122 @@ runtime = {}) {
|
|
|
3597
3705
|
chatScopes.closeHeld(id, ticket);
|
|
3598
3706
|
runtime.broadcast?.(frame);
|
|
3599
3707
|
}
|
|
3600
|
-
|
|
3601
|
-
|
|
3708
|
+
afterFold?.();
|
|
3709
|
+
if (path?.runId !== undefined) {
|
|
3710
|
+
const runId = path.runId;
|
|
3711
|
+
const cancelled = await adapter.cancelRun(runId).then(() => null, (err) => message(err));
|
|
3712
|
+
if (cancelled !== null)
|
|
3713
|
+
return { ok: true, cancelError: { runId, error: cancelled } };
|
|
3714
|
+
}
|
|
3602
3715
|
return { ok: true };
|
|
3603
3716
|
}
|
|
3604
3717
|
catch (err) {
|
|
3605
|
-
return
|
|
3718
|
+
return { ok: false, error: message(err) };
|
|
3606
3719
|
}
|
|
3607
3720
|
finally {
|
|
3608
3721
|
// Never left held: a fold that threw frees the id here (its own close settles it otherwise).
|
|
3609
3722
|
chatScopes.closeHeld(id, ticket);
|
|
3610
3723
|
}
|
|
3724
|
+
};
|
|
3725
|
+
app.delete(`${V}/chats/:id`, async (req, reply) => {
|
|
3726
|
+
const { id } = req.params;
|
|
3727
|
+
// DES-SYSTEM-CHAT: DELETE also deletes the chat's history and attachments (POST /close keeps them)
|
|
3728
|
+
// — under `auth: required` only its opener's (another actor's chat is not this caller's to end).
|
|
3729
|
+
const viewer = security.authMode === 'required' ? actorOf(req).id : null;
|
|
3730
|
+
if (chatHistory !== null && chatHistory.entry(id) !== null && !chatHistory.visibleTo(id, viewer)) {
|
|
3731
|
+
return reply.code(404).send({ error: `no chat ${id} on this daemon` });
|
|
3732
|
+
}
|
|
3733
|
+
const removeHistory = () => {
|
|
3734
|
+
chatHistory?.remove(id);
|
|
3735
|
+
chatAttachments.removeChat(id);
|
|
3736
|
+
};
|
|
3737
|
+
const ended = await endChat(id, removeHistory);
|
|
3738
|
+
if (!ended.ok)
|
|
3739
|
+
return reply.code(400).send({ error: ended.error });
|
|
3740
|
+
// A chat the daemon no longer held (no fold ran): its kept history goes too.
|
|
3741
|
+
if (chatHistory?.entry(id) !== null && chatScopes.get(id) === undefined && askPaths.get(id) === undefined)
|
|
3742
|
+
removeHistory();
|
|
3743
|
+
return { ok: true };
|
|
3744
|
+
});
|
|
3745
|
+
// DES-SYSTEM-CHAT (api-types 0.137.0): end the chat and KEEP its history and attachments.
|
|
3746
|
+
app.post(`${V}/chats/:id/close`, { config: { manifest: { responseType: 'ChatCloseResponse', statusCodes: [200, 400, 404, 501, 502] } } }, async (req, reply) => {
|
|
3747
|
+
const { id } = req.params;
|
|
3748
|
+
if (chatHistory === null)
|
|
3749
|
+
return reply.code(501).send({ error: 'this daemon keeps no chat history' });
|
|
3750
|
+
const viewer = security.authMode === 'required' ? actorOf(req).id : null;
|
|
3751
|
+
const known = chatHistory.entry(id) !== null;
|
|
3752
|
+
if ((!known && askPaths.get(id) === undefined) || (known && !chatHistory.visibleTo(id, viewer))) {
|
|
3753
|
+
return reply.code(404).send({ error: `no chat ${id} on this daemon` });
|
|
3754
|
+
}
|
|
3755
|
+
// The close fold archives it; closing here too (before the id is free) covers a route set
|
|
3756
|
+
// without the fold, and is a no-op after it.
|
|
3757
|
+
const ended = await endChat(id, () => chatHistory.closed(id));
|
|
3758
|
+
if (!ended.ok)
|
|
3759
|
+
return reply.code(400).send({ error: ended.error });
|
|
3760
|
+
const chat = chatHistory.entry(id);
|
|
3761
|
+
if (chat === null)
|
|
3762
|
+
return reply.code(404).send({ error: `chat ${id} has no history on this daemon` });
|
|
3763
|
+
if (ended.cancelError !== undefined) {
|
|
3764
|
+
// The chat is ended and its history kept, but its run was NOT cancelled: said, never "ok".
|
|
3765
|
+
return reply.code(502).send({
|
|
3766
|
+
error: `chat ${id} is closed and its history kept, but its run ${ended.cancelError.runId} could not be cancelled (${ended.cancelError.error}) — cancel it from the run`,
|
|
3767
|
+
runId: ended.cancelError.runId,
|
|
3768
|
+
chat,
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3771
|
+
return { ok: true, chat };
|
|
3772
|
+
});
|
|
3773
|
+
// DES-SYSTEM-CHAT (api-types 0.137.0): reopen a chat — its records, live or kept.
|
|
3774
|
+
app.get(`${V}/chats/:id/messages`, { config: { manifest: { responseType: 'ChatMessagesResponse', statusCodes: [200, 404, 501] } } }, async (req, reply) => {
|
|
3775
|
+
const { id } = req.params;
|
|
3776
|
+
if (chatHistory === null)
|
|
3777
|
+
return reply.code(501).send({ error: 'this daemon keeps no chat history' });
|
|
3778
|
+
const viewer = security.authMode === 'required' ? actorOf(req).id : null;
|
|
3779
|
+
const chat = chatHistory.visibleTo(id, viewer)
|
|
3780
|
+
? chatHistory.entry(id, askPaths.get(id)?.pa ?? null, (cid) => askPaths.get(cid) !== undefined || chatScopes.get(cid) !== undefined)
|
|
3781
|
+
: null;
|
|
3782
|
+
const messages = chat === null ? null : chatHistory.messages(id);
|
|
3783
|
+
if (chat === null || messages === null)
|
|
3784
|
+
return reply.code(404).send({ error: `no chat ${id} in this daemon's history` });
|
|
3785
|
+
return { chat, messages, attachments: chatAttachments.list(id) };
|
|
3786
|
+
});
|
|
3787
|
+
// DES-SYSTEM-CHAT (api-types 0.137.0): upload files to a chat (multipart/form-data). The body is
|
|
3788
|
+
// buffered by a parser scoped to this route alone, capped a little over the per-message total;
|
|
3789
|
+
// the per-file and per-message caps, and the types (decided from the bytes), are checked before
|
|
3790
|
+
// anything is stored.
|
|
3791
|
+
void app.register(async (scope) => {
|
|
3792
|
+
scope.addContentTypeParser('multipart/form-data', { parseAs: 'buffer', bodyLimit: ATTACHMENT_MAX_MESSAGE_BYTES + 1024 * 1024 }, (_req, body, done) => done(null, body));
|
|
3793
|
+
scope.setErrorHandler((err, _req, reply) => {
|
|
3794
|
+
if (err.code === 'FST_ERR_CTP_BODY_TOO_LARGE') {
|
|
3795
|
+
return reply.code(413).send(new AttachmentError(413, 'attachments_too_large', `the upload is over the ${Math.round(ATTACHMENT_MAX_MESSAGE_BYTES / 1048576)} MiB limit per message`).body());
|
|
3796
|
+
}
|
|
3797
|
+
if (err.code === 'FST_ERR_CTP_INVALID_MEDIA_TYPE') {
|
|
3798
|
+
return reply.code(400).send(new AttachmentError(400, 'attachment_malformed', 'upload files as multipart/form-data').body());
|
|
3799
|
+
}
|
|
3800
|
+
return reply.send(err);
|
|
3801
|
+
});
|
|
3802
|
+
scope.post(`${V}/chats/:id/attachments`, { config: { manifest: { requestType: 'multipart/form-data', responseType: 'ChatAttachmentUploadResponse', statusCodes: [201, 400, 404, 413, 415] } } }, async (req, reply) => {
|
|
3803
|
+
const { id } = req.params;
|
|
3804
|
+
// Only a chat open on this daemon takes files (a message can only be sent on one), and
|
|
3805
|
+
// under `auth: required` only its opener's.
|
|
3806
|
+
const viewer = security.authMode === 'required' ? actorOf(req).id : null;
|
|
3807
|
+
if (askPaths.get(id) === undefined || (chatHistory !== null && chatHistory.entry(id) !== null && !chatHistory.visibleTo(id, viewer))) {
|
|
3808
|
+
return reply.code(404).send({ error: `no open chat ${id} on this daemon` });
|
|
3809
|
+
}
|
|
3810
|
+
const boundary = boundaryOf(req.headers['content-type']);
|
|
3811
|
+
if (boundary === null || !Buffer.isBuffer(req.body)) {
|
|
3812
|
+
return reply.code(400).send(new AttachmentError(400, 'attachment_malformed', 'upload files as multipart/form-data with a boundary').body());
|
|
3813
|
+
}
|
|
3814
|
+
try {
|
|
3815
|
+
const attachments = chatAttachments.add(id, parseMultipart(req.body, boundary));
|
|
3816
|
+
return reply.code(201).send({ attachments, limits: ATTACHMENT_LIMITS });
|
|
3817
|
+
}
|
|
3818
|
+
catch (err) {
|
|
3819
|
+
if (err instanceof AttachmentError)
|
|
3820
|
+
return reply.code(err.status).send(err.body());
|
|
3821
|
+
throw err;
|
|
3822
|
+
}
|
|
3823
|
+
});
|
|
3611
3824
|
});
|
|
3612
3825
|
// A unit's captured transcript. `unitKey` accepts the fully-qualified id (`run-1:survey`),
|
|
3613
3826
|
// the id suffix (`survey`, `u3`), or the ordinal (`u3`, `3`) — see `resolveUnit`.
|
|
@@ -6918,6 +7131,235 @@ runtime = {}) {
|
|
|
6918
7131
|
// F-043: the bridge emits where this daemon's interactive seams read — one bus per daemon.
|
|
6919
7132
|
busDataDir: runtime.interactiveBridgeBusDataDir ?? null,
|
|
6920
7133
|
});
|
|
7134
|
+
// ── GET /system — the Nerve Center snapshot (DES-SYSTEM-VIEW, api-types 0.136.0) ──
|
|
7135
|
+
// One derived, READ-ONLY answer: the CLIs with their 7-day record, every unfinished run with a
|
|
7136
|
+
// COMPUTED liveness, the infrastructure as real bounded probes, the governance instruments and
|
|
7137
|
+
// `attention[]`. Every probe runs per request (no cached "ok"); only the 7-day fold over the run
|
|
7138
|
+
// store is kept for SYSTEM_STATS_TTL_MS, with its `asOf`. The whole answer is held to
|
|
7139
|
+
// SYSTEM_BUDGET_MS: a part that does not settle in time reads `unknown` (or `null`) with why.
|
|
7140
|
+
const weekFoldCache = new WeekFoldCache();
|
|
7141
|
+
/** One stat of a run's durable event log: read before the 7-day fold, so the fold can never eat
|
|
7142
|
+
* the time a liveness verdict needs. */
|
|
7143
|
+
const EVIDENCE_TIMEOUT_MS = 1_000;
|
|
7144
|
+
const systemExec = runtime.systemExec ?? ((file, args, opts) => execCapped(file, args, { timeout: opts.timeout }));
|
|
7145
|
+
app.get(`${V}/system`, { config: { manifest: { responseType: 'SystemSnapshot', statusCodes: [200] } } }, async () => {
|
|
7146
|
+
const startedAt = Date.now();
|
|
7147
|
+
const deadline = startedAt + SYSTEM_BUDGET_MS - 500;
|
|
7148
|
+
const left = () => Math.max(0, deadline - Date.now());
|
|
7149
|
+
const dbPath = typeof adapter.dbPath === 'string' && adapter.dbPath !== '' && adapter.dbPath !== ':memory:' ? adapter.dbPath : null;
|
|
7150
|
+
const stateHome = dbPath !== null ? stateHomeOfDb(dbPath) : null;
|
|
7151
|
+
const containerFloor = isContainerSeatFloor();
|
|
7152
|
+
const roster = (() => {
|
|
7153
|
+
try {
|
|
7154
|
+
return markOptIn(rosterWithStanding());
|
|
7155
|
+
}
|
|
7156
|
+
catch {
|
|
7157
|
+
return [];
|
|
7158
|
+
}
|
|
7159
|
+
})();
|
|
7160
|
+
// Reads the folds and the run list depend on, each bounded.
|
|
7161
|
+
const viewsP = settleWithin(async () => {
|
|
7162
|
+
const detail = await adapter.sessionsDetail();
|
|
7163
|
+
await backfillEndedAt(detail);
|
|
7164
|
+
return detail.map(decorateRun);
|
|
7165
|
+
}, PROBE_TIMEOUT_MS);
|
|
7166
|
+
const settingsP = settleWithin(() => Promise.resolve(adapter.getSettings()), PROBE_TIMEOUT_MS);
|
|
7167
|
+
const gatesP = settleWithin(async () => (typeof adapter.interactionRequests === 'function' ? await adapter.interactionRequests(undefined, 'open') : null), PROBE_TIMEOUT_MS);
|
|
7168
|
+
const diagP = settleWithin(() => diagnosticsNow(), PROBE_TIMEOUT_MS);
|
|
7169
|
+
const pingP = settleWithin(() => enginePing(), PROBE_TIMEOUT_MS);
|
|
7170
|
+
const rulesP = settleWithin(async () => {
|
|
7171
|
+
if (typeof adapter.listConformanceRules !== 'function')
|
|
7172
|
+
return null;
|
|
7173
|
+
const rules = await adapter.listConformanceRules();
|
|
7174
|
+
return rules.filter((r) => r.retired !== true).length;
|
|
7175
|
+
}, PROBE_TIMEOUT_MS);
|
|
7176
|
+
const gatesByYouP = settleWithin(async () => {
|
|
7177
|
+
const since = Date.now() - SYSTEM_STATS_DAYS * 86_400_000;
|
|
7178
|
+
const entries = await audit.readAll({ action: 'gate.decided', since });
|
|
7179
|
+
return entries.filter((e) => e.actor?.kind === 'human').length;
|
|
7180
|
+
}, PROBE_TIMEOUT_MS);
|
|
7181
|
+
// The infra probes, all at once.
|
|
7182
|
+
const gatewayDir = stateHome !== null ? gatewayDirOf(join(stateHome, 'gateway')) : null;
|
|
7183
|
+
const interactiveRoots = [...new Set([defaultInteractiveRoot(stateHome ?? undefined), ...interactiveBridges.keys()])];
|
|
7184
|
+
const watch = runtime.watchRegistry?.() ?? null;
|
|
7185
|
+
const watchHealth = watch !== null ? (() => { try {
|
|
7186
|
+
return watch.health();
|
|
7187
|
+
}
|
|
7188
|
+
catch {
|
|
7189
|
+
return null;
|
|
7190
|
+
} })() : null;
|
|
7191
|
+
const infraP = Promise.all([
|
|
7192
|
+
probeGateway({ containerFloor, gatewayDir, runtime: containerFloor ? resolveContainerRuntime() : null, exec: systemExec }),
|
|
7193
|
+
probeBroker({
|
|
7194
|
+
containerFloor,
|
|
7195
|
+
clis: roster.map((r) => r.key).filter((k) => isContainerLoginCli(k)),
|
|
7196
|
+
status: (cli) => containerStatus.refreshFresh(cli),
|
|
7197
|
+
}),
|
|
7198
|
+
probeInteractive({
|
|
7199
|
+
roots: interactiveRoots,
|
|
7200
|
+
readLock: (root) => readBridgeLockAsync(root),
|
|
7201
|
+
health: async (url, timeoutMs) => (await fetch(url, { signal: AbortSignal.timeout(timeoutMs) })).status,
|
|
7202
|
+
}),
|
|
7203
|
+
probeEstate({ memoryStore: memoryStoreInfo(), call: estateReadonlyCall(memoryStoreInfo().path) }),
|
|
7204
|
+
probeBus({
|
|
7205
|
+
unavailable: adapter.busUnavailable ?? null,
|
|
7206
|
+
seamsOff: adapter.busSeamsOff ?? null,
|
|
7207
|
+
dbPath: typeof adapter.busDbPath === 'string' && adapter.busDbPath !== '' ? adapter.busDbPath : null,
|
|
7208
|
+
watchBus: watchHealth?.sources.bus ?? null,
|
|
7209
|
+
tailLagMs: watchHealth?.tail_lag_ms ?? null,
|
|
7210
|
+
}),
|
|
7211
|
+
probeVault({ vault: adapter.vaultBin != null ? vaultDiagnostics(adapter.vaultBin) : null, exec: systemExec }),
|
|
7212
|
+
diskRow(stateHome),
|
|
7213
|
+
]);
|
|
7214
|
+
const [views, settings, openGates, diag, ping, rules, gatesByYou, infraProbed] = await Promise.all([
|
|
7215
|
+
viewsP, settingsP, gatesP, diagP, pingP, rulesP, gatesByYouP, infraP,
|
|
7216
|
+
]);
|
|
7217
|
+
// The broker probe refreshed each container seat's sign-in reading: the CLI cards read the
|
|
7218
|
+
// roster AFTER it, so one snapshot never says "broker: signed in" beside "claude: signed out".
|
|
7219
|
+
const rosterNow = (() => {
|
|
7220
|
+
try {
|
|
7221
|
+
return markOptIn(rosterWithStanding());
|
|
7222
|
+
}
|
|
7223
|
+
catch {
|
|
7224
|
+
return roster;
|
|
7225
|
+
}
|
|
7226
|
+
})();
|
|
7227
|
+
const allViews = views.kind === 'ok' ? views.value : [];
|
|
7228
|
+
// runs[] with computed liveness.
|
|
7229
|
+
const { quietAfterMs, stallAfterMs } = livenessThresholds(settings.kind === 'ok' ? settings.value?.workerStallMinutes : undefined);
|
|
7230
|
+
const openByRun = new Map();
|
|
7231
|
+
if (openGates.kind === 'ok' && openGates.value !== null) {
|
|
7232
|
+
const at = (r) => new Date(r.created_at).getTime() || 0;
|
|
7233
|
+
for (const r of [...openGates.value].sort((a, b) => at(a) - at(b))) {
|
|
7234
|
+
openByRun.set(r.session_id, { interactionId: r.id, ord: r.ord, kind: r.gate_kind ?? (r.kind === 'elicitation' ? 'question' : null), prompt: r.prompt.slice(0, 280) });
|
|
7235
|
+
}
|
|
7236
|
+
}
|
|
7237
|
+
const now = Date.now();
|
|
7238
|
+
const live = unfinished(allViews);
|
|
7239
|
+
const evidenceOf = async (id) => {
|
|
7240
|
+
let best = null;
|
|
7241
|
+
const relay = runtime.runEvidence?.(id);
|
|
7242
|
+
if (relay !== undefined)
|
|
7243
|
+
best = { at: relay.at, source: relay.source };
|
|
7244
|
+
if (dbPath !== null) {
|
|
7245
|
+
const st = await settleWithin(() => fsp.stat(join(eventsDirOf(dbPath), `${id}.ndjson`)), EVIDENCE_TIMEOUT_MS);
|
|
7246
|
+
if (st.kind === 'ok' && (best === null || st.value.mtimeMs > best.at))
|
|
7247
|
+
best = { at: Math.floor(st.value.mtimeMs), source: 'event_log' };
|
|
7248
|
+
}
|
|
7249
|
+
return best;
|
|
7250
|
+
};
|
|
7251
|
+
const runPairs = await Promise.all(live.map(async (view) => {
|
|
7252
|
+
const cached = gateCache.get(view.session.id);
|
|
7253
|
+
const gate = openByRun.get(view.session.id) ??
|
|
7254
|
+
(view.session.status === 'awaiting_human' && cached !== undefined
|
|
7255
|
+
? { interactionId: null, ord: cached.ord, kind: cached.gateKind ?? null, prompt: cached.prompt.slice(0, 280) }
|
|
7256
|
+
: null);
|
|
7257
|
+
const run = runLiveness({ view, now, evidence: await evidenceOf(view.session.id), gate: view.session.status === 'awaiting_human' ? gate : null, quietAfterMs, stallAfterMs });
|
|
7258
|
+
return { view, run };
|
|
7259
|
+
}));
|
|
7260
|
+
// The 7-day fold: cached briefly, never waited on past the budget.
|
|
7261
|
+
const readEvents = async (id) => {
|
|
7262
|
+
const engine = await adapter.runEvents(id);
|
|
7263
|
+
if (engine === null)
|
|
7264
|
+
return null;
|
|
7265
|
+
const daemon = runtime.stallFrames?.(id) ?? [];
|
|
7266
|
+
return daemon.length === 0 ? engine : [...engine, ...daemon];
|
|
7267
|
+
};
|
|
7268
|
+
const week = views.kind === 'ok' && typeof adapter.runEvents === 'function'
|
|
7269
|
+
? await settleWithin(() => weekFoldCache.get(() => readWeek(allViews, readEvents, { days: SYSTEM_STATS_DAYS, now: Date.now() })), left())
|
|
7270
|
+
: null;
|
|
7271
|
+
const weekFold = week?.kind === 'ok' ? week.value : null;
|
|
7272
|
+
const weekNote = views.kind !== 'ok'
|
|
7273
|
+
? `the engine did not list its runs within ${PROBE_TIMEOUT_MS / 1000} s`
|
|
7274
|
+
: week === null
|
|
7275
|
+
? 'the run store has no event-log read binding on this engine'
|
|
7276
|
+
: week.kind === 'timeout'
|
|
7277
|
+
? 'the 7-day fold did not finish inside this snapshot’s bound; it keeps going and the next read has it'
|
|
7278
|
+
: week.kind === 'error'
|
|
7279
|
+
? `the 7-day fold failed: ${week.error}`
|
|
7280
|
+
: weekFold === null
|
|
7281
|
+
? 'the run store has no event-log read binding on this engine'
|
|
7282
|
+
: undefined;
|
|
7283
|
+
// Diagnostics blocks, for the rows that carry them.
|
|
7284
|
+
const d = diag.kind === 'ok' ? diag.value : null;
|
|
7285
|
+
const seatGov = d?.seatGovernance.seats ?? [];
|
|
7286
|
+
const clis = buildClis({
|
|
7287
|
+
roster: rosterNow,
|
|
7288
|
+
acp: (d?.acp.byCli ?? {}),
|
|
7289
|
+
seatGovernance: seatGov,
|
|
7290
|
+
containerFloor,
|
|
7291
|
+
hostBoundary: d?.seatGovernance.hostBoundary ?? hostSandboxLauncher(),
|
|
7292
|
+
week: weekFold,
|
|
7293
|
+
...(weekNote !== undefined ? { weekNote } : {}),
|
|
7294
|
+
});
|
|
7295
|
+
const runs = sortRuns(runPairs.map((p) => p.run));
|
|
7296
|
+
const gaps = judgeGaps(runPairs, clis);
|
|
7297
|
+
// infra[]: the daemon row first (it answers this request; the engine's ping says if it is free).
|
|
7298
|
+
const daemonStarted = Date.now() - Math.max(views.ms, ping.ms);
|
|
7299
|
+
const pingWord = ping.kind === 'ok' ? ping.value : ping.kind === 'timeout' ? 'no answer' : `error: ${ping.error}`;
|
|
7300
|
+
const daemonDetails = {
|
|
7301
|
+
version: PKG_VERSION,
|
|
7302
|
+
ping: pingWord,
|
|
7303
|
+
warnings: healthWarningsOf(runtime.stateHome !== undefined ? (d?.stateHome ?? null) : null, runtime.skills?.baseSkill() ?? null),
|
|
7304
|
+
...(d !== null ? { components: d.components, daemon: d.daemon, recentErrors: d.recentErrors } : {}),
|
|
7305
|
+
};
|
|
7306
|
+
const daemonRow = views.kind !== 'ok'
|
|
7307
|
+
? infraRow('daemon', 'degraded', `serving, but the engine did not list its runs (${views.kind === 'timeout' ? `no answer within ${PROBE_TIMEOUT_MS / 1000} s` : views.error}): runs[] and the CLI stats are incomplete`, 'Re-check; if the engine stays busy, a large launch may be planning.', daemonStarted, daemonDetails, PROBE_TIMEOUT_MS)
|
|
7308
|
+
: ping.kind === 'ok' && ping.value === ENGINE_BUSY_PING
|
|
7309
|
+
? infraRow('daemon', 'degraded', 'serving; the engine is busy (its ping did not answer within 1 s)', 'Re-check in a minute; a large launch may be planning.', daemonStarted, daemonDetails)
|
|
7310
|
+
: ping.kind !== 'ok'
|
|
7311
|
+
? infraRow('daemon', 'unknown', `serving; the engine ping gave ${pingWord}`, 'Re-check.', daemonStarted, daemonDetails, PROBE_TIMEOUT_MS)
|
|
7312
|
+
: infraRow('daemon', 'ok', `serving crew ${PKG_VERSION}; engine ping ${ping.value}`, null, daemonStarted, daemonDetails);
|
|
7313
|
+
const storesRow = d === null
|
|
7314
|
+
? infraRow('stores', 'unknown', `diagnostics did not answer within ${PROBE_TIMEOUT_MS / 1000} s`, 'Re-check.', startedAt, {}, PROBE_TIMEOUT_MS)
|
|
7315
|
+
: dbPath === null
|
|
7316
|
+
? infraRow('stores', 'unknown', 'this route set has no core store', null, startedAt, { stores: d.stores })
|
|
7317
|
+
: infraRow('stores', 'ok', `${d.stores.length} store file(s), ${Math.round(d.stores.reduce((a, s) => a + s.bytes, 0) / 2 ** 20)} MiB`, null, startedAt, { stores: d.stores });
|
|
7318
|
+
const floorWord = containerFloor ? 'container' : isHostSeatFloor() ? 'host' : 'unset';
|
|
7319
|
+
const roleFindings = d?.seatGovernance.roleFindings ?? [];
|
|
7320
|
+
const seatFloorRow = d === null
|
|
7321
|
+
? infraRow('seat_floor', 'unknown', `diagnostics did not answer (${diag.kind === 'timeout' ? `no answer within ${PROBE_TIMEOUT_MS / 1000} s` : diag.kind === 'error' ? diag.error : ''})`, 'Re-check.', startedAt, { floor: floorWord }, PROBE_TIMEOUT_MS)
|
|
7322
|
+
: infraRow('seat_floor', floorWord === 'host' && (d?.seatGovernance.hostBoundary ?? null) === null ? 'degraded' : roleFindings.length > 0 ? 'degraded' : 'ok', floorWord === 'container'
|
|
7323
|
+
? 'container floor: governed seats run in hardened containers'
|
|
7324
|
+
: floorWord === 'host'
|
|
7325
|
+
? (d?.seatGovernance.hostBoundary ?? null) === null
|
|
7326
|
+
? 'interim host floor, and no OS launcher (sandbox-exec / bwrap) on PATH: floor seats run advisory'
|
|
7327
|
+
: `interim host floor, bounded by ${d?.seatGovernance.hostBoundary}`
|
|
7328
|
+
: 'no seat floor chosen', roleFindings.length > 0 ? (roleFindings[0]?.message ?? 'see the role findings') : 'Install the OS launcher, or run the container floor.', startedAt, { floor: floorWord, rosterFile: d?.seatGovernance.rosterFile ?? null, hostBoundary: d?.seatGovernance.hostBoundary ?? null, roleFindings });
|
|
7329
|
+
const infra = [
|
|
7330
|
+
daemonRow,
|
|
7331
|
+
...infraProbed.slice(0, 6),
|
|
7332
|
+
d !== null ? skillsRow(d.skills) : infraRow('skills', 'unknown', 'diagnostics did not answer in time', 'Re-check.', startedAt),
|
|
7333
|
+
stateHomeRow(d === null ? null : (d.stateHome ?? null)),
|
|
7334
|
+
storesRow,
|
|
7335
|
+
seatFloorRow,
|
|
7336
|
+
infraProbed[6],
|
|
7337
|
+
loadRow(),
|
|
7338
|
+
];
|
|
7339
|
+
const governance = buildGovernance({
|
|
7340
|
+
governance: (d?.governance ?? null),
|
|
7341
|
+
...(d === null ? { governanceNote: `diagnostics did not answer (${diag.kind === 'timeout' ? `no answer within ${PROBE_TIMEOUT_MS / 1000} s` : diag.kind === 'error' ? diag.error : ''})` } : {}),
|
|
7342
|
+
skillsFindings: (d?.skills.findings ?? []),
|
|
7343
|
+
decisions: d?.decisions ?? null,
|
|
7344
|
+
clis,
|
|
7345
|
+
judgeGapCount: gaps.length,
|
|
7346
|
+
week: weekFold,
|
|
7347
|
+
...(weekNote !== undefined ? { weekNote } : {}),
|
|
7348
|
+
gatesByYou: gatesByYou.kind === 'ok' ? gatesByYou.value : null,
|
|
7349
|
+
steeringRules: rules.kind === 'ok' ? rules.value : null,
|
|
7350
|
+
...(rules.kind === 'ok' && rules.value === null ? { steeringNote: 'this engine cannot list steering rules' } : rules.kind === 'timeout' ? { steeringNote: `the rule store did not answer within ${PROBE_TIMEOUT_MS / 1000} s` } : rules.kind === 'error' ? { steeringNote: `the rule store could not be read: ${rules.error}` } : {}),
|
|
7351
|
+
});
|
|
7352
|
+
const working = runs.filter((r) => r.liveness !== 'waiting_on_you' && r.liveness !== 'held').length;
|
|
7353
|
+
const attention = buildAttention({
|
|
7354
|
+
infra,
|
|
7355
|
+
clis,
|
|
7356
|
+
runs,
|
|
7357
|
+
governance,
|
|
7358
|
+
gaps,
|
|
7359
|
+
blockedByInfra: (key) => key === 'gateway' || key === 'broker' ? (containerFloor ? working : 0) : key === 'skills' || key === 'state_home' || key === 'disk' || key === 'load' ? working : 0,
|
|
7360
|
+
});
|
|
7361
|
+
return assembleSnapshot({ startedAt, clis, runs, infra, governance, attention });
|
|
7362
|
+
});
|
|
6921
7363
|
registerInteractiveProxy(app, adapter, {
|
|
6922
7364
|
settings: projectSettings,
|
|
6923
7365
|
pool: interactiveBridges,
|