syntaur 0.66.1 → 0.68.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/.claude-plugin/plugin.json +1 -1
- package/dist/dashboard/server.js +568 -60
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +1267 -699
- package/dist/index.js.map +1 -1
- package/dist/launch/index.d.ts +18 -0
- package/dist/launch/index.js +111 -7
- package/dist/launch/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
- package/platforms/claude-code/hooks/hooks.json +1 -1
- package/platforms/claude-code/hooks/session-cleanup.sh +21 -0
- package/platforms/codex/.codex-plugin/plugin.json +1 -1
- package/platforms/codex/hooks.json +1 -1
- package/platforms/codex/scripts/session-cleanup.sh +13 -0
- package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
- package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
package/dist/dashboard/server.js
CHANGED
|
@@ -937,6 +937,38 @@ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
|
937
937
|
});
|
|
938
938
|
|
|
939
939
|
// src/lifecycle/event-emit.ts
|
|
940
|
+
var event_emit_exports = {};
|
|
941
|
+
__export(event_emit_exports, {
|
|
942
|
+
emitEvent: () => emitEvent,
|
|
943
|
+
isSuppressingEvents: () => isSuppressingEvents,
|
|
944
|
+
recordStatusEvent: () => recordStatusEvent,
|
|
945
|
+
resolveActor: () => resolveActor,
|
|
946
|
+
setSuppressEvents: () => setSuppressEvents,
|
|
947
|
+
withSuppressedEvents: () => withSuppressedEvents
|
|
948
|
+
});
|
|
949
|
+
function setSuppressEvents(value) {
|
|
950
|
+
suppressEvents = value;
|
|
951
|
+
}
|
|
952
|
+
function isSuppressingEvents() {
|
|
953
|
+
return suppressEvents;
|
|
954
|
+
}
|
|
955
|
+
function withSuppressedEvents(fn) {
|
|
956
|
+
const prior = suppressEvents;
|
|
957
|
+
suppressEvents = true;
|
|
958
|
+
try {
|
|
959
|
+
const result = fn();
|
|
960
|
+
if (result instanceof Promise) {
|
|
961
|
+
return result.finally(() => {
|
|
962
|
+
suppressEvents = prior;
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
suppressEvents = prior;
|
|
966
|
+
return result;
|
|
967
|
+
} catch (e) {
|
|
968
|
+
suppressEvents = prior;
|
|
969
|
+
throw e;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
940
972
|
function resolveActor(by) {
|
|
941
973
|
return by ?? "system";
|
|
942
974
|
}
|
|
@@ -952,6 +984,10 @@ function recordStatusEvent(input) {
|
|
|
952
984
|
details: { from: input.from, to: input.to, command: input.command }
|
|
953
985
|
});
|
|
954
986
|
}
|
|
987
|
+
function emitEvent(input) {
|
|
988
|
+
if (suppressEvents) return;
|
|
989
|
+
recordEvent(input);
|
|
990
|
+
}
|
|
955
991
|
var suppressEvents;
|
|
956
992
|
var init_event_emit = __esm({
|
|
957
993
|
"src/lifecycle/event-emit.ts"() {
|
|
@@ -3764,7 +3800,9 @@ __export(config_exports, {
|
|
|
3764
3800
|
getTerminal: () => getTerminal,
|
|
3765
3801
|
normalizeFactDeclarations: () => normalizeFactDeclarations,
|
|
3766
3802
|
parseAgentCommand: () => parseAgentCommand,
|
|
3803
|
+
parseDurationMs: () => parseDurationMs,
|
|
3767
3804
|
parseSearchConfig: () => parseSearchConfig,
|
|
3805
|
+
parseStalenessConfig: () => parseStalenessConfig,
|
|
3768
3806
|
parseStatusConfig: () => parseStatusConfig,
|
|
3769
3807
|
parseTerminalConfig: () => parseTerminalConfig,
|
|
3770
3808
|
readConfig: () => readConfig,
|
|
@@ -3780,6 +3818,7 @@ __export(config_exports, {
|
|
|
3780
3818
|
validateDeriveConfig: () => validateDeriveConfig,
|
|
3781
3819
|
validateDeriveShape: () => validateDeriveShape,
|
|
3782
3820
|
validateFactDeclarations: () => validateFactDeclarations,
|
|
3821
|
+
validateStalenessConfig: () => validateStalenessConfig,
|
|
3783
3822
|
writeAgentsConfig: () => writeAgentsConfig,
|
|
3784
3823
|
writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
|
|
3785
3824
|
writeSearchConfig: () => writeSearchConfig,
|
|
@@ -3791,6 +3830,13 @@ __export(config_exports, {
|
|
|
3791
3830
|
import { readFile as readFile5 } from "fs/promises";
|
|
3792
3831
|
import { spawnSync } from "child_process";
|
|
3793
3832
|
import { resolve as resolve7, isAbsolute } from "path";
|
|
3833
|
+
function parseDurationMs(raw2) {
|
|
3834
|
+
const m = raw2.trim().match(DURATION_RE);
|
|
3835
|
+
if (!m) return null;
|
|
3836
|
+
const n = Number(m[1]);
|
|
3837
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
3838
|
+
return n * DURATION_UNIT_MS[m[2] ?? "ms"];
|
|
3839
|
+
}
|
|
3794
3840
|
function parseAgentCommand(value, agentId) {
|
|
3795
3841
|
if (typeof value !== "string" || value.trim() === "") {
|
|
3796
3842
|
throw new AgentConfigError(
|
|
@@ -5027,6 +5073,65 @@ ${cleanedFm}
|
|
|
5027
5073
|
---${afterFrontmatter}`;
|
|
5028
5074
|
await writeFileForce(configPath, newContent);
|
|
5029
5075
|
}
|
|
5076
|
+
function parseStalenessConfig(content) {
|
|
5077
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
5078
|
+
if (!match) return null;
|
|
5079
|
+
const fmBlock = match[1];
|
|
5080
|
+
const blockStart = fmBlock.match(/^staleness:\s*$/m);
|
|
5081
|
+
if (!blockStart) return null;
|
|
5082
|
+
const startIdx = (blockStart.index ?? 0) + blockStart[0].length;
|
|
5083
|
+
const lines = fmBlock.slice(startIdx).split("\n");
|
|
5084
|
+
const out = {};
|
|
5085
|
+
for (const line of lines) {
|
|
5086
|
+
if (line.trim() === "") continue;
|
|
5087
|
+
const trimmed = line.trimStart();
|
|
5088
|
+
const indent = line.length - trimmed.length;
|
|
5089
|
+
if (indent === 0) break;
|
|
5090
|
+
const ci = trimmed.indexOf(":");
|
|
5091
|
+
if (ci <= 0) continue;
|
|
5092
|
+
const key = trimmed.slice(0, ci).trim();
|
|
5093
|
+
const field = STALENESS_KEY_TO_FIELD[key];
|
|
5094
|
+
if (!field) continue;
|
|
5095
|
+
let value = trimmed.slice(ci + 1).trim();
|
|
5096
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
5097
|
+
value = value.slice(1, -1);
|
|
5098
|
+
}
|
|
5099
|
+
const ms = parseDurationMs(value);
|
|
5100
|
+
if (ms !== null) out[field] = ms;
|
|
5101
|
+
}
|
|
5102
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
5103
|
+
}
|
|
5104
|
+
function validateStalenessConfig(content) {
|
|
5105
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
5106
|
+
if (!match) return [];
|
|
5107
|
+
const fmBlock = match[1];
|
|
5108
|
+
const blockStart = fmBlock.match(/^staleness:\s*$/m);
|
|
5109
|
+
if (!blockStart) return [];
|
|
5110
|
+
const startIdx = (blockStart.index ?? 0) + blockStart[0].length;
|
|
5111
|
+
const lines = fmBlock.slice(startIdx).split("\n");
|
|
5112
|
+
const problems = [];
|
|
5113
|
+
for (const line of lines) {
|
|
5114
|
+
if (line.trim() === "") continue;
|
|
5115
|
+
const trimmed = line.trimStart();
|
|
5116
|
+
const indent = line.length - trimmed.length;
|
|
5117
|
+
if (indent === 0) break;
|
|
5118
|
+
const ci = trimmed.indexOf(":");
|
|
5119
|
+
if (ci <= 0) continue;
|
|
5120
|
+
const key = trimmed.slice(0, ci).trim();
|
|
5121
|
+
let value = trimmed.slice(ci + 1).trim();
|
|
5122
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
5123
|
+
value = value.slice(1, -1);
|
|
5124
|
+
}
|
|
5125
|
+
if (!(key in STALENESS_KEY_TO_FIELD)) {
|
|
5126
|
+
problems.push(`staleness.${key}: unknown key (expected one of ${Object.keys(STALENESS_KEY_TO_FIELD).join(", ")})`);
|
|
5127
|
+
continue;
|
|
5128
|
+
}
|
|
5129
|
+
if (parseDurationMs(value) === null) {
|
|
5130
|
+
problems.push(`staleness.${key}: "${value}" is not a positive duration (e.g. 7d, 12h, 30m, 90s, 500ms)`);
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
return problems;
|
|
5134
|
+
}
|
|
5030
5135
|
function parseSearchConfig(content) {
|
|
5031
5136
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
5032
5137
|
if (!match) return null;
|
|
@@ -5309,7 +5414,9 @@ async function readConfig() {
|
|
|
5309
5414
|
}
|
|
5310
5415
|
})(),
|
|
5311
5416
|
searchConfig: parseSearchConfig(content),
|
|
5312
|
-
workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock)
|
|
5417
|
+
workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock),
|
|
5418
|
+
staleness: parseStalenessConfig(content),
|
|
5419
|
+
stalenessWatchdog: String(fm["stalenessWatchdog"]).toLowerCase() === "true"
|
|
5313
5420
|
};
|
|
5314
5421
|
}
|
|
5315
5422
|
function getAssignmentTypes(config) {
|
|
@@ -5372,7 +5479,7 @@ async function updateAgentsConfig(mutation, options = {}) {
|
|
|
5372
5479
|
await writeAgentsConfig(next);
|
|
5373
5480
|
return { previous, next, written: true };
|
|
5374
5481
|
}
|
|
5375
|
-
var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, SESSION_AUTO_TRACK_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
|
|
5482
|
+
var STALENESS_KEY_TO_FIELD, DURATION_RE, DURATION_UNIT_MS, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, SESSION_AUTO_TRACK_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
|
|
5376
5483
|
var init_config2 = __esm({
|
|
5377
5484
|
"src/utils/config.ts"() {
|
|
5378
5485
|
"use strict";
|
|
@@ -5389,6 +5496,21 @@ var init_config2 = __esm({
|
|
|
5389
5496
|
init_terminal_schema();
|
|
5390
5497
|
init_search_schema();
|
|
5391
5498
|
init_workspace_visibility_schema();
|
|
5499
|
+
STALENESS_KEY_TO_FIELD = {
|
|
5500
|
+
inProgressNoActivity: "inProgressNoActivityMs",
|
|
5501
|
+
readyUnclaimed: "readyUnclaimedMs",
|
|
5502
|
+
reviewAging: "reviewAgingMs",
|
|
5503
|
+
blockedAging: "blockedAgingMs",
|
|
5504
|
+
planApprovalAging: "planApprovalAgingMs"
|
|
5505
|
+
};
|
|
5506
|
+
DURATION_RE = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/;
|
|
5507
|
+
DURATION_UNIT_MS = {
|
|
5508
|
+
ms: 1,
|
|
5509
|
+
s: 1e3,
|
|
5510
|
+
m: 6e4,
|
|
5511
|
+
h: 36e5,
|
|
5512
|
+
d: 864e5
|
|
5513
|
+
};
|
|
5392
5514
|
DEFAULT_ASSIGNMENT_TYPES = {
|
|
5393
5515
|
definitions: [
|
|
5394
5516
|
{ id: "feature", label: "Feature" },
|
|
@@ -5431,7 +5553,9 @@ var init_config2 = __esm({
|
|
|
5431
5553
|
searchConfig: null,
|
|
5432
5554
|
workspaceVisibility: {
|
|
5433
5555
|
hidden: []
|
|
5434
|
-
}
|
|
5556
|
+
},
|
|
5557
|
+
staleness: null,
|
|
5558
|
+
stalenessWatchdog: false
|
|
5435
5559
|
};
|
|
5436
5560
|
AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
|
|
5437
5561
|
SESSION_AUTO_TRACK_VALUES = ["all", "workspaces-only", "off"];
|
|
@@ -7793,6 +7917,74 @@ var init_overviewCopy = __esm({
|
|
|
7793
7917
|
}
|
|
7794
7918
|
});
|
|
7795
7919
|
|
|
7920
|
+
// src/staleness/classify.ts
|
|
7921
|
+
function resolveStaleThresholds(overrides) {
|
|
7922
|
+
const merged = { ...DEFAULT_STALE_THRESHOLDS };
|
|
7923
|
+
if (overrides) {
|
|
7924
|
+
for (const key of Object.keys(merged)) {
|
|
7925
|
+
const v = overrides[key];
|
|
7926
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) merged[key] = v;
|
|
7927
|
+
}
|
|
7928
|
+
}
|
|
7929
|
+
return merged;
|
|
7930
|
+
}
|
|
7931
|
+
function classifyNeedsAttention(input, thresholds = DEFAULT_STALE_THRESHOLDS) {
|
|
7932
|
+
if (input.isTerminal) return [];
|
|
7933
|
+
const reasons = [];
|
|
7934
|
+
const age = input.statusAgeMs;
|
|
7935
|
+
const aged = (gate) => age !== null && age >= gate;
|
|
7936
|
+
const blocked = input.disposition === "blocked" || input.blockedReason !== null;
|
|
7937
|
+
if (input.phase === IN_PROGRESS_PHASE && !blocked && aged(thresholds.inProgressNoActivityMs) && input.lastActivityMs !== null && input.lastActivityMs >= thresholds.inProgressNoActivityMs) {
|
|
7938
|
+
reasons.push({
|
|
7939
|
+
kind: "in_progress_no_activity",
|
|
7940
|
+
label: "In progress, but no recent activity",
|
|
7941
|
+
severity: "medium"
|
|
7942
|
+
});
|
|
7943
|
+
}
|
|
7944
|
+
if (input.phase === READY_PHASE && input.assignee === null && aged(thresholds.readyUnclaimedMs)) {
|
|
7945
|
+
reasons.push({
|
|
7946
|
+
kind: "ready_unclaimed",
|
|
7947
|
+
label: "Ready to implement, unclaimed",
|
|
7948
|
+
severity: "medium"
|
|
7949
|
+
});
|
|
7950
|
+
}
|
|
7951
|
+
if (input.phase === REVIEW_PHASE && aged(thresholds.reviewAgingMs)) {
|
|
7952
|
+
reasons.push({ kind: "review_aging", label: "Awaiting review", severity: "high" });
|
|
7953
|
+
}
|
|
7954
|
+
if (blocked && aged(thresholds.blockedAgingMs)) {
|
|
7955
|
+
reasons.push({ kind: "blocked_aging", label: "Blocked and aging", severity: "high" });
|
|
7956
|
+
}
|
|
7957
|
+
if (input.phase === PLANNING_PHASE && input.planExists && !input.planApproved && aged(thresholds.planApprovalAgingMs)) {
|
|
7958
|
+
reasons.push({
|
|
7959
|
+
kind: "plan_awaiting_approval",
|
|
7960
|
+
label: "Plan awaiting approval",
|
|
7961
|
+
severity: "medium"
|
|
7962
|
+
});
|
|
7963
|
+
}
|
|
7964
|
+
if (input.depsSatisfied === false && (input.phase === READY_PHASE || input.phase === IN_PROGRESS_PHASE)) {
|
|
7965
|
+
reasons.push({ kind: "deps_unsatisfied", label: "Unmet dependencies", severity: "high" });
|
|
7966
|
+
}
|
|
7967
|
+
return reasons;
|
|
7968
|
+
}
|
|
7969
|
+
var DAY, DEFAULT_STALE_THRESHOLDS, PLANNING_PHASE, READY_PHASE, IN_PROGRESS_PHASE, REVIEW_PHASE;
|
|
7970
|
+
var init_classify = __esm({
|
|
7971
|
+
"src/staleness/classify.ts"() {
|
|
7972
|
+
"use strict";
|
|
7973
|
+
DAY = 24 * 60 * 60 * 1e3;
|
|
7974
|
+
DEFAULT_STALE_THRESHOLDS = {
|
|
7975
|
+
inProgressNoActivityMs: 7 * DAY,
|
|
7976
|
+
readyUnclaimedMs: 3 * DAY,
|
|
7977
|
+
reviewAgingMs: 3 * DAY,
|
|
7978
|
+
blockedAgingMs: 3 * DAY,
|
|
7979
|
+
planApprovalAgingMs: 3 * DAY
|
|
7980
|
+
};
|
|
7981
|
+
PLANNING_PHASE = "ready_for_planning";
|
|
7982
|
+
READY_PHASE = "ready_to_implement";
|
|
7983
|
+
IN_PROGRESS_PHASE = "in_progress";
|
|
7984
|
+
REVIEW_PHASE = "review";
|
|
7985
|
+
}
|
|
7986
|
+
});
|
|
7987
|
+
|
|
7796
7988
|
// src/dashboard/servers.ts
|
|
7797
7989
|
import { readdir as readdir9, readFile as readFile12, unlink as unlink2 } from "fs/promises";
|
|
7798
7990
|
import { resolve as resolve15 } from "path";
|
|
@@ -8427,7 +8619,38 @@ var init_scanner = __esm({
|
|
|
8427
8619
|
});
|
|
8428
8620
|
|
|
8429
8621
|
// src/dashboard/api.ts
|
|
8430
|
-
|
|
8622
|
+
var api_exports = {};
|
|
8623
|
+
__export(api_exports, {
|
|
8624
|
+
WorkspaceBlockedError: () => WorkspaceBlockedError,
|
|
8625
|
+
clearStatusConfigCache: () => clearStatusConfigCache,
|
|
8626
|
+
collectStaleCandidates: () => collectStaleCandidates,
|
|
8627
|
+
createWorkspace: () => createWorkspace,
|
|
8628
|
+
deleteWorkspace: () => deleteWorkspace,
|
|
8629
|
+
getAssignmentDetail: () => getAssignmentDetail,
|
|
8630
|
+
getAssignmentDetailById: () => getAssignmentDetailById,
|
|
8631
|
+
getEditableDocument: () => getEditableDocument,
|
|
8632
|
+
getEditableDocumentById: () => getEditableDocumentById,
|
|
8633
|
+
getHelp: () => getHelp,
|
|
8634
|
+
getMemoryDetail: () => getMemoryDetail,
|
|
8635
|
+
getOverview: () => getOverview,
|
|
8636
|
+
getPlaybookDetail: () => getPlaybookDetail,
|
|
8637
|
+
getProjectDetail: () => getProjectDetail,
|
|
8638
|
+
getResourceDetail: () => getResourceDetail,
|
|
8639
|
+
getStatusConfig: () => getStatusConfig,
|
|
8640
|
+
installRecordsInvalidation: () => installRecordsInvalidation,
|
|
8641
|
+
invalidateRecordsCache: () => invalidateRecordsCache,
|
|
8642
|
+
listAllMemories: () => listAllMemories,
|
|
8643
|
+
listAllResources: () => listAllResources,
|
|
8644
|
+
listArchived: () => listArchived,
|
|
8645
|
+
listAssignmentsBoard: () => listAssignmentsBoard,
|
|
8646
|
+
listPlaybooks: () => listPlaybooks,
|
|
8647
|
+
listProjects: () => listProjects,
|
|
8648
|
+
listWorkspaceRecords: () => listWorkspaceRecords,
|
|
8649
|
+
listWorkspaces: () => listWorkspaces,
|
|
8650
|
+
resolveProjectPath: () => resolveProjectPath,
|
|
8651
|
+
resolveWorkspaceMembers: () => resolveWorkspaceMembers
|
|
8652
|
+
});
|
|
8653
|
+
import { readdir as readdir10, readFile as readFile13, writeFile as writeFile3, stat as stat2 } from "fs/promises";
|
|
8431
8654
|
import { resolve as resolve17, dirname as dirname2, basename } from "path";
|
|
8432
8655
|
function clearFrontmatterField(content, key) {
|
|
8433
8656
|
const fieldRegex = new RegExp(`^(${escapeRegExp2(key)}:)\\s*.*$`, "m");
|
|
@@ -8818,10 +9041,10 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
|
|
|
8818
9041
|
(total, record) => total + (record.summary.progress["failed"] ?? 0),
|
|
8819
9042
|
0
|
|
8820
9043
|
),
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
9044
|
+
// Derived from the SAME classifier verdict as the stale segment (via the
|
|
9045
|
+
// pre-cap segment total) so the badge count can never diverge from the
|
|
9046
|
+
// listed rows.
|
|
9047
|
+
staleAssignments: segments.stale.total
|
|
8825
9048
|
},
|
|
8826
9049
|
hero,
|
|
8827
9050
|
segments,
|
|
@@ -9973,9 +10196,77 @@ function segmentSeverity(segment) {
|
|
|
9973
10196
|
return "medium";
|
|
9974
10197
|
}
|
|
9975
10198
|
}
|
|
10199
|
+
function topStaleReason(reasons) {
|
|
10200
|
+
if (reasons.length === 0) return null;
|
|
10201
|
+
return reasons.slice().sort((a, b) => STALE_SEVERITY_RANK[b.severity] - STALE_SEVERITY_RANK[a.severity])[0];
|
|
10202
|
+
}
|
|
10203
|
+
async function readProgressActivityMs(progressPath, now) {
|
|
10204
|
+
try {
|
|
10205
|
+
const s = await stat2(progressPath);
|
|
10206
|
+
return Math.max(0, now - s.mtimeMs);
|
|
10207
|
+
} catch {
|
|
10208
|
+
return null;
|
|
10209
|
+
}
|
|
10210
|
+
}
|
|
10211
|
+
function classifyAssignmentRecord(assignment, terminalStatuses, depsSatisfied, lastActivityMs, thresholds) {
|
|
10212
|
+
const virtuals = deriveStatusVirtuals(assignment, terminalStatuses);
|
|
10213
|
+
return classifyNeedsAttention(
|
|
10214
|
+
{
|
|
10215
|
+
phase: virtuals.phase,
|
|
10216
|
+
disposition: virtuals.disposition,
|
|
10217
|
+
isTerminal: terminalStatuses.has(assignment.status),
|
|
10218
|
+
assignee: assignment.assignee ?? null,
|
|
10219
|
+
blockedReason: assignment.blockedReason,
|
|
10220
|
+
depsSatisfied,
|
|
10221
|
+
// plan_awaiting_approval is deferred to the decision inbox's plan-approval
|
|
10222
|
+
// category for now; pass values that keep that reason dormant.
|
|
10223
|
+
planExists: false,
|
|
10224
|
+
planApproved: true,
|
|
10225
|
+
statusAgeMs: virtuals.statusAge,
|
|
10226
|
+
lastActivityMs
|
|
10227
|
+
},
|
|
10228
|
+
thresholds
|
|
10229
|
+
);
|
|
10230
|
+
}
|
|
10231
|
+
async function collectStaleCandidates(projectsDir, assignmentsDir2) {
|
|
10232
|
+
const [projectRecords, standaloneRecords] = await Promise.all([
|
|
10233
|
+
listProjectRecords(projectsDir),
|
|
10234
|
+
listStandaloneRecords(assignmentsDir2)
|
|
10235
|
+
]);
|
|
10236
|
+
const { terminalStatuses } = await getStatusConfig();
|
|
10237
|
+
const thresholds = resolveStaleThresholds((await readConfig()).staleness);
|
|
10238
|
+
const now = Date.now();
|
|
10239
|
+
const out = [];
|
|
10240
|
+
for (const record of projectRecords) {
|
|
10241
|
+
if (isProjectArchived(record.summary)) continue;
|
|
10242
|
+
const projectPath = resolve17(projectsDir, record.summary.slug);
|
|
10243
|
+
const depMap = /* @__PURE__ */ new Map();
|
|
10244
|
+
for (const a of record.assignments) depMap.set(a.slug, a.status);
|
|
10245
|
+
for (const assignment of activeAssignments(record.assignments)) {
|
|
10246
|
+
const depsSatisfied = assignment.dependsOn.length === 0 ? true : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;
|
|
10247
|
+
const lastActivityMs = await readProgressActivityMs(
|
|
10248
|
+
resolve17(projectPath, "assignments", assignment.slug, "progress.md"),
|
|
10249
|
+
now
|
|
10250
|
+
);
|
|
10251
|
+
const reasons = classifyAssignmentRecord(assignment, terminalStatuses, depsSatisfied, lastActivityMs, thresholds);
|
|
10252
|
+
if (reasons.length > 0) {
|
|
10253
|
+
out.push({ assignmentId: assignment.id, projectSlug: record.summary.slug, reasons });
|
|
10254
|
+
}
|
|
10255
|
+
}
|
|
10256
|
+
}
|
|
10257
|
+
for (const sr of standaloneRecords) {
|
|
10258
|
+
if (sr.record.archived === true) continue;
|
|
10259
|
+
const lastActivityMs = await readProgressActivityMs(resolve17(sr.assignmentDir, "progress.md"), now);
|
|
10260
|
+
const reasons = classifyAssignmentRecord(sr.record, terminalStatuses, true, lastActivityMs, thresholds);
|
|
10261
|
+
if (reasons.length > 0) out.push({ assignmentId: sr.record.id, projectSlug: null, reasons });
|
|
10262
|
+
}
|
|
10263
|
+
return out;
|
|
10264
|
+
}
|
|
9976
10265
|
async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords, traces) {
|
|
9977
10266
|
const now = Date.now();
|
|
9978
10267
|
const buckets = emptyBuckets();
|
|
10268
|
+
const { terminalStatuses } = await getStatusConfig();
|
|
10269
|
+
const staleThresholds = resolveStaleThresholds((await readConfig()).staleness);
|
|
9979
10270
|
const newestPool = [];
|
|
9980
10271
|
for (const record of projectRecords) {
|
|
9981
10272
|
if (isProjectArchived(record.summary)) continue;
|
|
@@ -9984,6 +10275,7 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
|
|
|
9984
10275
|
depMap.set(a.slug, a.status);
|
|
9985
10276
|
}
|
|
9986
10277
|
const visibleAssignments = activeAssignments(record.assignments);
|
|
10278
|
+
const projectPath = resolve17(projectsDir, record.summary.slug);
|
|
9987
10279
|
const resolvedTransitions = await Promise.all(
|
|
9988
10280
|
visibleAssignments.map(async (assignment) => {
|
|
9989
10281
|
const t0 = traces ? performance.now() : 0;
|
|
@@ -9995,13 +10287,25 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
|
|
|
9995
10287
|
{ traces, dependencyStatusMap: depMap }
|
|
9996
10288
|
);
|
|
9997
10289
|
if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
|
|
9998
|
-
|
|
10290
|
+
const depsSatisfied = assignment.dependsOn.length === 0 ? true : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;
|
|
10291
|
+
const lastActivityMs = await readProgressActivityMs(
|
|
10292
|
+
resolve17(projectPath, "assignments", assignment.slug, "progress.md"),
|
|
10293
|
+
now
|
|
10294
|
+
);
|
|
10295
|
+
return { assignment, availableTransitions, depsSatisfied, lastActivityMs };
|
|
9999
10296
|
})
|
|
10000
10297
|
);
|
|
10001
|
-
for (const { assignment, availableTransitions } of resolvedTransitions) {
|
|
10298
|
+
for (const { assignment, availableTransitions, depsSatisfied, lastActivityMs } of resolvedTransitions) {
|
|
10002
10299
|
const segmentId = STATUS_TO_SEGMENT[assignment.status];
|
|
10003
|
-
const
|
|
10004
|
-
const
|
|
10300
|
+
const isTerminal = terminalStatuses.has(assignment.status);
|
|
10301
|
+
const staleReasons = classifyAssignmentRecord(
|
|
10302
|
+
assignment,
|
|
10303
|
+
terminalStatuses,
|
|
10304
|
+
depsSatisfied,
|
|
10305
|
+
lastActivityMs,
|
|
10306
|
+
staleThresholds
|
|
10307
|
+
);
|
|
10308
|
+
const stale = staleReasons.length > 0;
|
|
10005
10309
|
const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
|
|
10006
10310
|
const baseId = `${record.summary.slug}:${assignment.slug}`;
|
|
10007
10311
|
const shared = {
|
|
@@ -10030,11 +10334,12 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
|
|
|
10030
10334
|
buckets[segmentId].push(primary);
|
|
10031
10335
|
}
|
|
10032
10336
|
if (stale && !isTerminal) {
|
|
10337
|
+
const top = topStaleReason(staleReasons);
|
|
10033
10338
|
const staleItem = {
|
|
10034
10339
|
...shared,
|
|
10035
10340
|
id: `${baseId}:stale`,
|
|
10036
10341
|
severity: "low",
|
|
10037
|
-
reason: SEGMENT_REASON.stale,
|
|
10342
|
+
reason: top?.label ?? SEGMENT_REASON.stale,
|
|
10038
10343
|
segment: "stale"
|
|
10039
10344
|
};
|
|
10040
10345
|
buckets.stale.push(staleItem);
|
|
@@ -10058,14 +10363,22 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
|
|
|
10058
10363
|
const t0 = traces ? performance.now() : 0;
|
|
10059
10364
|
const availableTransitions = await getStandaloneAvailableTransitions(sr.record);
|
|
10060
10365
|
if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
|
|
10061
|
-
|
|
10366
|
+
const lastActivityMs = await readProgressActivityMs(resolve17(sr.assignmentDir, "progress.md"), now);
|
|
10367
|
+
return { sr, availableTransitions, lastActivityMs };
|
|
10062
10368
|
})
|
|
10063
10369
|
);
|
|
10064
|
-
for (const { sr, availableTransitions } of resolvedStandaloneTransitions) {
|
|
10370
|
+
for (const { sr, availableTransitions, lastActivityMs } of resolvedStandaloneTransitions) {
|
|
10065
10371
|
const assignment = sr.record;
|
|
10066
10372
|
const segmentId = STATUS_TO_SEGMENT[assignment.status];
|
|
10067
|
-
const
|
|
10068
|
-
const
|
|
10373
|
+
const isTerminal = terminalStatuses.has(assignment.status);
|
|
10374
|
+
const staleReasons = classifyAssignmentRecord(
|
|
10375
|
+
assignment,
|
|
10376
|
+
terminalStatuses,
|
|
10377
|
+
true,
|
|
10378
|
+
lastActivityMs,
|
|
10379
|
+
staleThresholds
|
|
10380
|
+
);
|
|
10381
|
+
const stale = staleReasons.length > 0;
|
|
10069
10382
|
const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
|
|
10070
10383
|
const baseId = `standalone:${sr.id}`;
|
|
10071
10384
|
const shared = {
|
|
@@ -10093,11 +10406,12 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
|
|
|
10093
10406
|
});
|
|
10094
10407
|
}
|
|
10095
10408
|
if (stale && !isTerminal) {
|
|
10409
|
+
const top = topStaleReason(staleReasons);
|
|
10096
10410
|
buckets.stale.push({
|
|
10097
10411
|
...shared,
|
|
10098
10412
|
id: `${baseId}:stale`,
|
|
10099
10413
|
severity: "low",
|
|
10100
|
-
reason: SEGMENT_REASON.stale,
|
|
10414
|
+
reason: top?.label ?? SEGMENT_REASON.stale,
|
|
10101
10415
|
segment: "stale"
|
|
10102
10416
|
});
|
|
10103
10417
|
}
|
|
@@ -10218,13 +10532,6 @@ function parseTimestamp(timestamp) {
|
|
|
10218
10532
|
const parsed = Date.parse(timestamp);
|
|
10219
10533
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
10220
10534
|
}
|
|
10221
|
-
function isStale(updated) {
|
|
10222
|
-
const timestamp = parseTimestamp(updated);
|
|
10223
|
-
if (timestamp === 0) {
|
|
10224
|
-
return false;
|
|
10225
|
-
}
|
|
10226
|
-
return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
|
|
10227
|
-
}
|
|
10228
10535
|
async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
10229
10536
|
const commentsPath = resolve17(
|
|
10230
10537
|
projectPath,
|
|
@@ -10343,7 +10650,7 @@ async function getPlaybookDetail(playbooksDir2, slug) {
|
|
|
10343
10650
|
enabled
|
|
10344
10651
|
};
|
|
10345
10652
|
}
|
|
10346
|
-
var WorkspaceBlockedError,
|
|
10653
|
+
var WorkspaceBlockedError, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, STATUS_TO_SEGMENT, HERO_PRIORITY, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS, STALE_SEVERITY_RANK;
|
|
10347
10654
|
var init_api = __esm({
|
|
10348
10655
|
"src/dashboard/api.ts"() {
|
|
10349
10656
|
"use strict";
|
|
@@ -10361,6 +10668,7 @@ var init_api = __esm({
|
|
|
10361
10668
|
init_help();
|
|
10362
10669
|
init_agent_sessions();
|
|
10363
10670
|
init_overviewCopy();
|
|
10671
|
+
init_classify();
|
|
10364
10672
|
WorkspaceBlockedError = class extends Error {
|
|
10365
10673
|
blockedBy;
|
|
10366
10674
|
constructor(blockedBy) {
|
|
@@ -10371,7 +10679,6 @@ var init_api = __esm({
|
|
|
10371
10679
|
this.blockedBy = blockedBy;
|
|
10372
10680
|
}
|
|
10373
10681
|
};
|
|
10374
|
-
STALE_ASSIGNMENT_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
10375
10682
|
RECENT_PROJECTS_LIMIT = 6;
|
|
10376
10683
|
RECENT_ACTIVITY_LIMIT = 12;
|
|
10377
10684
|
RECENT_SESSIONS_LIMIT = 10;
|
|
@@ -10379,7 +10686,6 @@ var init_api = __esm({
|
|
|
10379
10686
|
SEGMENT_DISPLAY_CAP = 5;
|
|
10380
10687
|
STALE_LIMIT_DEFAULT = 50;
|
|
10381
10688
|
STALE_LIMIT_MAX = 200;
|
|
10382
|
-
TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["completed", "failed", "archived"]);
|
|
10383
10689
|
STATUS_TO_SEGMENT = {
|
|
10384
10690
|
review: "readyForReview",
|
|
10385
10691
|
ready_to_implement: "readyToImplement",
|
|
@@ -10472,6 +10778,7 @@ var init_api = __esm({
|
|
|
10472
10778
|
failed: "fill:#9f2d2d,stroke:#651616,color:#ffffff",
|
|
10473
10779
|
review: "fill:#c6911e,stroke:#7a5a10,color:#ffffff"
|
|
10474
10780
|
};
|
|
10781
|
+
STALE_SEVERITY_RANK = { high: 3, medium: 2, low: 1 };
|
|
10475
10782
|
}
|
|
10476
10783
|
});
|
|
10477
10784
|
|
|
@@ -10907,12 +11214,13 @@ __export(recompute_exports, {
|
|
|
10907
11214
|
markDeriveMigrated: () => markDeriveMigrated,
|
|
10908
11215
|
recomputeAll: () => recomputeAll,
|
|
10909
11216
|
recomputeAndWrite: () => recomputeAndWrite,
|
|
11217
|
+
recomputeAssignmentDir: () => recomputeAssignmentDir,
|
|
10910
11218
|
recomputeDependents: () => recomputeDependents,
|
|
10911
11219
|
resolveDeriveContext: () => resolveDeriveContext
|
|
10912
11220
|
});
|
|
10913
11221
|
import { createHash as createHash2 } from "crypto";
|
|
10914
|
-
import { open, readdir as readdir12, readFile as readFile17, unlink as unlink5, stat as
|
|
10915
|
-
import { dirname as dirname4, resolve as resolve22 } from "path";
|
|
11222
|
+
import { open, readdir as readdir12, readFile as readFile17, unlink as unlink5, stat as stat3 } from "fs/promises";
|
|
11223
|
+
import { basename as basename3, dirname as dirname4, resolve as resolve22 } from "path";
|
|
10916
11224
|
async function isDeriveMigrated() {
|
|
10917
11225
|
return fileExists(resolve22(syntaurRoot(), MIGRATION_MARKER));
|
|
10918
11226
|
}
|
|
@@ -10952,7 +11260,7 @@ async function acquireLock(assignmentDir) {
|
|
|
10952
11260
|
const code = err.code;
|
|
10953
11261
|
if (code !== "EEXIST") throw err;
|
|
10954
11262
|
try {
|
|
10955
|
-
const info = await
|
|
11263
|
+
const info = await stat3(lockPath);
|
|
10956
11264
|
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
10957
11265
|
await unlink5(lockPath).catch(() => {
|
|
10958
11266
|
});
|
|
@@ -11144,6 +11452,18 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
|
|
|
11144
11452
|
}
|
|
11145
11453
|
return summary;
|
|
11146
11454
|
}
|
|
11455
|
+
async function recomputeAssignmentDir(assignmentDir, cause, by) {
|
|
11456
|
+
try {
|
|
11457
|
+
const assignmentPath = resolve22(assignmentDir, "assignment.md");
|
|
11458
|
+
if (!await fileExists(assignmentPath)) return null;
|
|
11459
|
+
const parent = dirname4(assignmentDir);
|
|
11460
|
+
const projectDir = basename3(parent) === "assignments" ? dirname4(parent) : null;
|
|
11461
|
+
const context = await resolveDeriveContext();
|
|
11462
|
+
return await recomputeAndWrite(assignmentPath, { cause, by, projectDir, context });
|
|
11463
|
+
} catch {
|
|
11464
|
+
return null;
|
|
11465
|
+
}
|
|
11466
|
+
}
|
|
11147
11467
|
var LOCK_FILE, LOCK_STALE_MS, LOCK_WAIT_MS, LOCK_MAX_WAITS, CAS_RETRIES, MIGRATION_MARKER;
|
|
11148
11468
|
var init_recompute = __esm({
|
|
11149
11469
|
"src/lifecycle/recompute.ts"() {
|
|
@@ -11251,14 +11571,14 @@ var init_process_info = __esm({
|
|
|
11251
11571
|
});
|
|
11252
11572
|
|
|
11253
11573
|
// src/usage/cwd-extractor.ts
|
|
11254
|
-
import { open as open3, readdir as readdir13, stat as
|
|
11574
|
+
import { open as open3, readdir as readdir13, stat as stat4 } from "fs/promises";
|
|
11255
11575
|
import { join as join4 } from "path";
|
|
11256
11576
|
import { homedir as homedir3 } from "os";
|
|
11257
11577
|
async function extractClaudeSessionMeta(jsonlPath) {
|
|
11258
11578
|
const cwd = await derivePathFromTranscript(jsonlPath);
|
|
11259
11579
|
if (!cwd) return null;
|
|
11260
|
-
const
|
|
11261
|
-
const sessionId =
|
|
11580
|
+
const basename7 = jsonlPath.split("/").pop() ?? "";
|
|
11581
|
+
const sessionId = basename7.replace(/\.jsonl$/, "");
|
|
11262
11582
|
if (!sessionId) return null;
|
|
11263
11583
|
const startTs = await readFirstTimestamp(jsonlPath);
|
|
11264
11584
|
const endTs = await readLastTimestamp(jsonlPath);
|
|
@@ -11353,8 +11673,8 @@ async function* walkClaudeProjects(opts = {}) {
|
|
|
11353
11673
|
async function* walkCodexSessions(opts = {}) {
|
|
11354
11674
|
const root = resolveCodexSessionsRoot(opts.root);
|
|
11355
11675
|
for await (const filePath of walkJsonlRecursive(root)) {
|
|
11356
|
-
const
|
|
11357
|
-
if (!
|
|
11676
|
+
const basename7 = filePath.split("/").pop() ?? "";
|
|
11677
|
+
if (!basename7.endsWith(".jsonl")) continue;
|
|
11358
11678
|
if (opts.sinceMtimeMs !== void 0) {
|
|
11359
11679
|
const mtime = await mtimeMs(filePath);
|
|
11360
11680
|
if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
|
|
@@ -11372,10 +11692,10 @@ function resolveCodexSessionsRoot(override) {
|
|
|
11372
11692
|
return join4(homedir3(), ".codex", "sessions");
|
|
11373
11693
|
}
|
|
11374
11694
|
async function extractPiSessionMeta(jsonlPath) {
|
|
11375
|
-
const
|
|
11376
|
-
const underscoreIdx =
|
|
11695
|
+
const basename7 = jsonlPath.split("/").pop() ?? "";
|
|
11696
|
+
const underscoreIdx = basename7.lastIndexOf("_");
|
|
11377
11697
|
if (underscoreIdx === -1) return null;
|
|
11378
|
-
const sessionId =
|
|
11698
|
+
const sessionId = basename7.slice(underscoreIdx + 1).replace(/\.jsonl$/, "");
|
|
11379
11699
|
if (!sessionId) return null;
|
|
11380
11700
|
let handle;
|
|
11381
11701
|
try {
|
|
@@ -11490,7 +11810,7 @@ async function* walkJsonlRecursive(root) {
|
|
|
11490
11810
|
}
|
|
11491
11811
|
async function mtimeMs(path) {
|
|
11492
11812
|
try {
|
|
11493
|
-
const s = await
|
|
11813
|
+
const s = await stat4(path);
|
|
11494
11814
|
return s.mtimeMs;
|
|
11495
11815
|
} catch {
|
|
11496
11816
|
return null;
|
|
@@ -11731,7 +12051,7 @@ function toDiscovered(meta) {
|
|
|
11731
12051
|
transcriptPath: meta.path
|
|
11732
12052
|
};
|
|
11733
12053
|
}
|
|
11734
|
-
var detectDir, claudeSessions, codexSessions, AGENT_TARGETS, AGENT_TARGETS_BY_ID;
|
|
12054
|
+
var detectDir, claudeSessions, codexSessions, piSessions, AGENT_TARGETS, AGENT_TARGETS_BY_ID;
|
|
11735
12055
|
var init_registry = __esm({
|
|
11736
12056
|
"src/targets/registry.ts"() {
|
|
11737
12057
|
"use strict";
|
|
@@ -11759,6 +12079,16 @@ var init_registry = __esm({
|
|
|
11759
12079
|
}
|
|
11760
12080
|
}
|
|
11761
12081
|
};
|
|
12082
|
+
piSessions = {
|
|
12083
|
+
globs: (root) => [join10(root ?? resolvePiSessionsRoot(), "*", "*.jsonl")],
|
|
12084
|
+
parse: async (file) => toDiscovered(await extractPiSessionMeta(file)),
|
|
12085
|
+
walk: async function* (opts = {}) {
|
|
12086
|
+
for await (const meta of walkPiSessions({ root: opts.root, sinceMtimeMs: opts.sinceMtimeMs })) {
|
|
12087
|
+
const d = toDiscovered(meta);
|
|
12088
|
+
if (d) yield d;
|
|
12089
|
+
}
|
|
12090
|
+
}
|
|
12091
|
+
};
|
|
11762
12092
|
AGENT_TARGETS = [
|
|
11763
12093
|
{
|
|
11764
12094
|
id: "cursor",
|
|
@@ -11815,6 +12145,7 @@ var init_registry = __esm({
|
|
|
11815
12145
|
detect: detectDir(home(".pi")),
|
|
11816
12146
|
skillsDir: { global: home(".pi", "agent", "skills") },
|
|
11817
12147
|
instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
|
|
12148
|
+
sessions: piSessions,
|
|
11818
12149
|
tier3: {
|
|
11819
12150
|
kind: "pi-extension",
|
|
11820
12151
|
source: "platforms/pi/extensions/syntaur",
|
|
@@ -12085,6 +12416,40 @@ var init_scanner2 = __esm({
|
|
|
12085
12416
|
}
|
|
12086
12417
|
});
|
|
12087
12418
|
|
|
12419
|
+
// src/staleness/watchdog.ts
|
|
12420
|
+
var watchdog_exports = {};
|
|
12421
|
+
__export(watchdog_exports, {
|
|
12422
|
+
runStalenessWatchdogTick: () => runStalenessWatchdogTick
|
|
12423
|
+
});
|
|
12424
|
+
function runStalenessWatchdogTick(candidates, seen, emit) {
|
|
12425
|
+
const staleNow = /* @__PURE__ */ new Map();
|
|
12426
|
+
for (const c of candidates) {
|
|
12427
|
+
if (c.reasons.length > 0) staleNow.set(c.assignmentId, c);
|
|
12428
|
+
}
|
|
12429
|
+
let newlyStale = 0;
|
|
12430
|
+
for (const [id, c] of staleNow) {
|
|
12431
|
+
if (!seen.has(id)) {
|
|
12432
|
+
seen.add(id);
|
|
12433
|
+
newlyStale++;
|
|
12434
|
+
emit({ assignmentId: id, projectSlug: c.projectSlug, type: "staleness-detected", reasons: c.reasons });
|
|
12435
|
+
}
|
|
12436
|
+
}
|
|
12437
|
+
let cleared = 0;
|
|
12438
|
+
for (const id of [...seen]) {
|
|
12439
|
+
if (!staleNow.has(id)) {
|
|
12440
|
+
seen.delete(id);
|
|
12441
|
+
cleared++;
|
|
12442
|
+
emit({ assignmentId: id, projectSlug: null, type: "staleness-cleared", reasons: [] });
|
|
12443
|
+
}
|
|
12444
|
+
}
|
|
12445
|
+
return { scanned: candidates.length, stale: staleNow.size, newlyStale, cleared };
|
|
12446
|
+
}
|
|
12447
|
+
var init_watchdog = __esm({
|
|
12448
|
+
"src/staleness/watchdog.ts"() {
|
|
12449
|
+
"use strict";
|
|
12450
|
+
}
|
|
12451
|
+
});
|
|
12452
|
+
|
|
12088
12453
|
// src/dashboard/server.ts
|
|
12089
12454
|
init_paths();
|
|
12090
12455
|
init_api();
|
|
@@ -13253,7 +13618,7 @@ init_timestamp();
|
|
|
13253
13618
|
init_fs();
|
|
13254
13619
|
init_git_worktree();
|
|
13255
13620
|
import { Router as Router2 } from "express";
|
|
13256
|
-
import { resolve as resolve23, basename as
|
|
13621
|
+
import { resolve as resolve23, basename as basename4, isAbsolute as isAbsolute4 } from "path";
|
|
13257
13622
|
import { rm, readFile as readFile18, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
|
|
13258
13623
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
13259
13624
|
|
|
@@ -14474,7 +14839,7 @@ function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
|
|
|
14474
14839
|
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
14475
14840
|
return;
|
|
14476
14841
|
}
|
|
14477
|
-
const document = await getEditableDocument(projectsDir, "memory",
|
|
14842
|
+
const document = await getEditableDocument(projectsDir, "memory", basename4(projectDir), itemSlug);
|
|
14478
14843
|
if (!document) {
|
|
14479
14844
|
res.status(404).json({ error: "Memory not found" });
|
|
14480
14845
|
return;
|
|
@@ -14493,7 +14858,7 @@ function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
|
|
|
14493
14858
|
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
14494
14859
|
return;
|
|
14495
14860
|
}
|
|
14496
|
-
const document = await getEditableDocument(projectsDir, "resource",
|
|
14861
|
+
const document = await getEditableDocument(projectsDir, "resource", basename4(projectDir), itemSlug);
|
|
14497
14862
|
if (!document) {
|
|
14498
14863
|
res.status(404).json({ error: "Resource not found" });
|
|
14499
14864
|
return;
|
|
@@ -14578,7 +14943,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
|
|
|
14578
14943
|
let content = renderItemStub(kind, {
|
|
14579
14944
|
slug: requestedSlug,
|
|
14580
14945
|
name,
|
|
14581
|
-
projectSlug:
|
|
14946
|
+
projectSlug: basename4(projectDir),
|
|
14582
14947
|
timestamp
|
|
14583
14948
|
});
|
|
14584
14949
|
const customBody = typeof body.body === "string" ? body.body : "";
|
|
@@ -14595,13 +14960,13 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
|
|
|
14595
14960
|
} catch (err) {
|
|
14596
14961
|
if (err.code === "EEXIST") {
|
|
14597
14962
|
res.status(409).json({
|
|
14598
|
-
error: `${kind === "memory" ? "Memory" : "Resource"} with slug "${requestedSlug}" already exists in project "${
|
|
14963
|
+
error: `${kind === "memory" ? "Memory" : "Resource"} with slug "${requestedSlug}" already exists in project "${basename4(projectDir)}".`
|
|
14599
14964
|
});
|
|
14600
14965
|
return;
|
|
14601
14966
|
}
|
|
14602
14967
|
throw err;
|
|
14603
14968
|
}
|
|
14604
|
-
res.status(201).json({ slug: requestedSlug, projectSlug:
|
|
14969
|
+
res.status(201).json({ slug: requestedSlug, projectSlug: basename4(projectDir), content });
|
|
14605
14970
|
} catch (error) {
|
|
14606
14971
|
console.error(`Error creating ${kind}:`, error);
|
|
14607
14972
|
res.status(500).json({ error: `Failed to create ${kind}: ${error.message}` });
|
|
@@ -14636,7 +15001,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
|
|
|
14636
15001
|
${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
14637
15002
|
merged = setTopLevelField(merged, "updated", nowTimestamp());
|
|
14638
15003
|
await writeFileForce(filePath, merged);
|
|
14639
|
-
const detail = await getItemDetail(kind,
|
|
15004
|
+
const detail = await getItemDetail(kind, basename4(projectDir), itemSlug);
|
|
14640
15005
|
res.json({ [kind]: detail, content: merged });
|
|
14641
15006
|
} catch (error) {
|
|
14642
15007
|
console.error(`Error updating ${kind}:`, error);
|
|
@@ -17854,7 +18219,7 @@ async function resolveSessionPlan(input, terminal) {
|
|
|
17854
18219
|
// src/launch/execute.ts
|
|
17855
18220
|
import { spawn as spawn3 } from "child_process";
|
|
17856
18221
|
import { homedir as homedir5 } from "os";
|
|
17857
|
-
import { basename as
|
|
18222
|
+
import { basename as basename5, join as join6, resolve as resolve26 } from "path";
|
|
17858
18223
|
init_fs();
|
|
17859
18224
|
init_config2();
|
|
17860
18225
|
init_session_id();
|
|
@@ -17874,7 +18239,7 @@ var TerminalNotFoundError = class extends Error {
|
|
|
17874
18239
|
var realSpawn = (command, args, options) => spawn3(command, args, options);
|
|
17875
18240
|
var WRAPPER_COMMANDS = /* @__PURE__ */ new Set(["osascript", "open", "sh"]);
|
|
17876
18241
|
function isWrapperCommand(command) {
|
|
17877
|
-
return WRAPPER_COMMANDS.has(
|
|
18242
|
+
return WRAPPER_COMMANDS.has(basename5(command));
|
|
17878
18243
|
}
|
|
17879
18244
|
var WRAPPER_EXIT_TIMEOUT_MS = 1500;
|
|
17880
18245
|
async function executeLaunchPlan(plan, spawnFn = realSpawn) {
|
|
@@ -19654,7 +20019,7 @@ async function readEvents(jobId) {
|
|
|
19654
20019
|
|
|
19655
20020
|
// src/schedules/attempt.ts
|
|
19656
20021
|
import { createHash as createHash3 } from "crypto";
|
|
19657
|
-
import { open as open4, readFile as readFile22, stat as
|
|
20022
|
+
import { open as open4, readFile as readFile22, stat as stat5, unlink as unlink6 } from "fs/promises";
|
|
19658
20023
|
import { resolve as resolve31 } from "path";
|
|
19659
20024
|
|
|
19660
20025
|
// src/schedules/triggers.ts
|
|
@@ -19851,7 +20216,7 @@ async function acquireJobLock(id) {
|
|
|
19851
20216
|
const code = err.code;
|
|
19852
20217
|
if (code !== "EEXIST") throw err;
|
|
19853
20218
|
try {
|
|
19854
|
-
const info = await
|
|
20219
|
+
const info = await stat5(lockPath);
|
|
19855
20220
|
if (Date.now() - info.mtimeMs > LOCK_STALE_MS2) {
|
|
19856
20221
|
await unlink6(lockPath).catch(() => {
|
|
19857
20222
|
});
|
|
@@ -21853,8 +22218,8 @@ init_api();
|
|
|
21853
22218
|
import { raw } from "express";
|
|
21854
22219
|
|
|
21855
22220
|
// src/todos/attachments.ts
|
|
21856
|
-
import { mkdir as mkdir4, readdir as readdir15, stat as
|
|
21857
|
-
import { resolve as resolve39, basename as
|
|
22221
|
+
import { mkdir as mkdir4, readdir as readdir15, stat as stat6, rename as rename5, rm as rm4, unlink as unlink7, writeFile as writeFile6, cp } from "fs/promises";
|
|
22222
|
+
import { resolve as resolve39, basename as basename6, dirname as dirname9, extname } from "path";
|
|
21858
22223
|
|
|
21859
22224
|
// src/utils/proof-artifact-id.ts
|
|
21860
22225
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -21927,7 +22292,7 @@ function isSafeInlineMime(mime) {
|
|
|
21927
22292
|
return SAFE_INLINE_MIME.has(mime);
|
|
21928
22293
|
}
|
|
21929
22294
|
function sanitizeAttachmentName(name) {
|
|
21930
|
-
let n =
|
|
22295
|
+
let n = basename6(name || "").replace(/["'\\/]/g, "_");
|
|
21931
22296
|
n = Array.from(n, (ch) => {
|
|
21932
22297
|
const code = ch.charCodeAt(0);
|
|
21933
22298
|
return code < 32 || code === 127 ? "_" : ch;
|
|
@@ -21950,7 +22315,7 @@ function attachmentDirFor(todosDir2, scopeId, todoId) {
|
|
|
21950
22315
|
}
|
|
21951
22316
|
async function dirExists(p) {
|
|
21952
22317
|
try {
|
|
21953
|
-
return (await
|
|
22318
|
+
return (await stat6(p)).isDirectory();
|
|
21954
22319
|
} catch {
|
|
21955
22320
|
return false;
|
|
21956
22321
|
}
|
|
@@ -21985,7 +22350,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
|
|
|
21985
22350
|
if (!ATTACHMENT_ID_RE.test(id)) continue;
|
|
21986
22351
|
const filename = stored.slice(sep2 + 2);
|
|
21987
22352
|
try {
|
|
21988
|
-
const st = await
|
|
22353
|
+
const st = await stat6(resolve39(dir, stored));
|
|
21989
22354
|
if (!st.isFile()) continue;
|
|
21990
22355
|
out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
|
|
21991
22356
|
} catch {
|
|
@@ -24151,7 +24516,7 @@ init_fs();
|
|
|
24151
24516
|
init_config2();
|
|
24152
24517
|
import { execFile as execFile2 } from "child_process";
|
|
24153
24518
|
import { promisify as promisify2 } from "util";
|
|
24154
|
-
import { cp as cp2, mkdtemp, rm as rm5, readFile as readFile29, writeFile as writeFile7, unlink as unlink8, stat as
|
|
24519
|
+
import { cp as cp2, mkdtemp, rm as rm5, readFile as readFile29, writeFile as writeFile7, unlink as unlink8, stat as stat7, open as open5, rename as rename8 } from "fs/promises";
|
|
24155
24520
|
import { resolve as resolve42, join as join9 } from "path";
|
|
24156
24521
|
import { tmpdir } from "os";
|
|
24157
24522
|
var exec2 = promisify2(execFile2);
|
|
@@ -24245,7 +24610,7 @@ async function cloneOrInit(repoUrl, destDir) {
|
|
|
24245
24610
|
}
|
|
24246
24611
|
async function copyRecursive(src, dest) {
|
|
24247
24612
|
if (!await fileExists(src)) return;
|
|
24248
|
-
const s = await
|
|
24613
|
+
const s = await stat7(src);
|
|
24249
24614
|
if (s.isDirectory()) {
|
|
24250
24615
|
await ensureDir(dest);
|
|
24251
24616
|
await cp2(src, dest, { recursive: true, force: true });
|
|
@@ -25101,6 +25466,41 @@ function runRollup() {
|
|
|
25101
25466
|
return { daysComputed, rowsWritten };
|
|
25102
25467
|
}
|
|
25103
25468
|
|
|
25469
|
+
// src/usage/pricing.ts
|
|
25470
|
+
var PER_MILLION = 1e6;
|
|
25471
|
+
var MODEL_PRICING = {
|
|
25472
|
+
// Moonshot Kimi K2.6 — the model pi emits today. Official Moonshot list price.
|
|
25473
|
+
// source: https://platform.moonshot.ai/ (official) — cross-checked
|
|
25474
|
+
// https://openrouter.ai/moonshotai/kimi-k2.6 (retrieved 2026-06-17)
|
|
25475
|
+
"moonshotai/kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0.95 },
|
|
25476
|
+
// Moonshot Kimi K2.5 — prior Kimi model. Official Moonshot list price.
|
|
25477
|
+
// source: https://platform.moonshot.ai/ — cross-checked
|
|
25478
|
+
// https://openrouter.ai/moonshotai/kimi-k2.5 (retrieved 2026-06-17)
|
|
25479
|
+
"moonshotai/kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0.6 },
|
|
25480
|
+
// Z.ai (Zhipu) GLM-5.2 — official z.ai platform list price.
|
|
25481
|
+
// source: https://docs.z.ai/guides/overview/pricing — cross-checked
|
|
25482
|
+
// https://openrouter.ai/z-ai/glm-5 (retrieved 2026-06-18)
|
|
25483
|
+
"zai-org/glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 1.4 },
|
|
25484
|
+
// MiniMax M2.5 — official MiniMax platform list price. No separately-pinned
|
|
25485
|
+
// cached-read rate is published, so cacheRead is set conservatively to the
|
|
25486
|
+
// input rate (upper bound); revise if MiniMax publishes a cache rate.
|
|
25487
|
+
// source: https://platform.minimax.io/docs/guides/pricing-token-plan
|
|
25488
|
+
// — cross-checked https://openrouter.ai/minimax/minimax-m2.5 (retrieved 2026-06-18)
|
|
25489
|
+
"minimaxai/minimax-m2.5": { input: 0.15, output: 0.9, cacheRead: 0.15, cacheWrite: 0.15 }
|
|
25490
|
+
// NOTE: opaque Synthetic tier aliases like `syn:large:text` have no public
|
|
25491
|
+
// per-token rate (they route to whatever Synthetic assigns), so they remain
|
|
25492
|
+
// unpriced (→ $0). Reseller discounts (e.g. DeepInfra K2.6 0.75/3.50/0.15) are
|
|
25493
|
+
// rejected by the canonical-source rule and are NOT used here.
|
|
25494
|
+
};
|
|
25495
|
+
function normalizeModelKey(model) {
|
|
25496
|
+
return model.replace(/^\s*\[[^\]]*\]\s*/, "").replace(/^hf:/i, "").trim().toLowerCase();
|
|
25497
|
+
}
|
|
25498
|
+
function priceForModel(model, tokens) {
|
|
25499
|
+
const rate = MODEL_PRICING[normalizeModelKey(model)];
|
|
25500
|
+
if (!rate) return null;
|
|
25501
|
+
return (tokens.inputTokens * rate.input + tokens.outputTokens * rate.output + tokens.cacheReadTokens * rate.cacheRead + tokens.cacheCreationTokens * rate.cacheWrite) / PER_MILLION;
|
|
25502
|
+
}
|
|
25503
|
+
|
|
25104
25504
|
// src/usage/collect.ts
|
|
25105
25505
|
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
25106
25506
|
function formatDayFromCcusageDate(yyyymmdd) {
|
|
@@ -25134,6 +25534,12 @@ async function collectAndPersist() {
|
|
|
25134
25534
|
cwd,
|
|
25135
25535
|
eventTs: row.eventTs
|
|
25136
25536
|
});
|
|
25537
|
+
const totalCost = row.totalCost > 0 ? row.totalCost : priceForModel(row.model, {
|
|
25538
|
+
inputTokens: row.inputTokens,
|
|
25539
|
+
outputTokens: row.outputTokens,
|
|
25540
|
+
cacheCreationTokens: row.cacheCreationTokens,
|
|
25541
|
+
cacheReadTokens: row.cacheReadTokens
|
|
25542
|
+
}) ?? 0;
|
|
25137
25543
|
return {
|
|
25138
25544
|
sessionId: row.sessionId,
|
|
25139
25545
|
model: row.model,
|
|
@@ -25144,7 +25550,7 @@ async function collectAndPersist() {
|
|
|
25144
25550
|
cacheCreationTokens: row.cacheCreationTokens,
|
|
25145
25551
|
cacheReadTokens: row.cacheReadTokens,
|
|
25146
25552
|
totalTokens: row.totalTokens,
|
|
25147
|
-
totalCost
|
|
25553
|
+
totalCost,
|
|
25148
25554
|
cwd,
|
|
25149
25555
|
projectSlug: attr.projectSlug ?? "",
|
|
25150
25556
|
assignmentSlug: attr.assignmentSlug ?? "",
|
|
@@ -25161,8 +25567,72 @@ async function collectAndPersist() {
|
|
|
25161
25567
|
tx.immediate();
|
|
25162
25568
|
return { isFirstRun, rowsIngested: enriched.length };
|
|
25163
25569
|
}
|
|
25570
|
+
function backfillZeroCostEvents() {
|
|
25571
|
+
const db5 = getUsageDb();
|
|
25572
|
+
const select = db5.prepare(
|
|
25573
|
+
`SELECT session_id, model, input_tokens, output_tokens,
|
|
25574
|
+
cache_creation_tokens, cache_read_tokens
|
|
25575
|
+
FROM usage_events
|
|
25576
|
+
WHERE total_cost = 0`
|
|
25577
|
+
);
|
|
25578
|
+
const update = db5.prepare(
|
|
25579
|
+
`UPDATE usage_events SET total_cost = @cost, updated_at = @updatedAt
|
|
25580
|
+
WHERE session_id = @sessionId AND model = @model AND total_cost = 0`
|
|
25581
|
+
);
|
|
25582
|
+
let updated = 0;
|
|
25583
|
+
const tx = db5.transaction(() => {
|
|
25584
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25585
|
+
for (const r of select.all()) {
|
|
25586
|
+
const cost = priceForModel(r.model, {
|
|
25587
|
+
inputTokens: r.input_tokens,
|
|
25588
|
+
outputTokens: r.output_tokens,
|
|
25589
|
+
cacheCreationTokens: r.cache_creation_tokens,
|
|
25590
|
+
cacheReadTokens: r.cache_read_tokens
|
|
25591
|
+
});
|
|
25592
|
+
if (cost !== null && cost > 0) {
|
|
25593
|
+
updated += update.run({ cost, updatedAt, sessionId: r.session_id, model: r.model }).changes;
|
|
25594
|
+
}
|
|
25595
|
+
}
|
|
25596
|
+
});
|
|
25597
|
+
tx.immediate();
|
|
25598
|
+
return updated;
|
|
25599
|
+
}
|
|
25600
|
+
function reattributeOrphanEvents() {
|
|
25601
|
+
const db5 = getUsageDb();
|
|
25602
|
+
const select = db5.prepare(
|
|
25603
|
+
`SELECT session_id, model, cwd, event_ts
|
|
25604
|
+
FROM usage_events
|
|
25605
|
+
WHERE project_slug = '' AND assignment_slug = ''`
|
|
25606
|
+
);
|
|
25607
|
+
const update = db5.prepare(
|
|
25608
|
+
`UPDATE usage_events
|
|
25609
|
+
SET project_slug = @projectSlug, assignment_slug = @assignmentSlug, updated_at = @updatedAt
|
|
25610
|
+
WHERE session_id = @sessionId AND model = @model
|
|
25611
|
+
AND project_slug = '' AND assignment_slug = ''`
|
|
25612
|
+
);
|
|
25613
|
+
let updated = 0;
|
|
25614
|
+
const tx = db5.transaction(() => {
|
|
25615
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25616
|
+
for (const r of select.all()) {
|
|
25617
|
+
const attr = resolveAttribution({
|
|
25618
|
+
sessionId: r.session_id,
|
|
25619
|
+
cwd: r.cwd,
|
|
25620
|
+
eventTs: r.event_ts
|
|
25621
|
+
});
|
|
25622
|
+
const projectSlug = attr.projectSlug ?? "";
|
|
25623
|
+
const assignmentSlug = attr.assignmentSlug ?? "";
|
|
25624
|
+
if (projectSlug !== "" || assignmentSlug !== "") {
|
|
25625
|
+
updated += update.run({ projectSlug, assignmentSlug, updatedAt, sessionId: r.session_id, model: r.model }).changes;
|
|
25626
|
+
}
|
|
25627
|
+
}
|
|
25628
|
+
});
|
|
25629
|
+
tx.immediate();
|
|
25630
|
+
return updated;
|
|
25631
|
+
}
|
|
25164
25632
|
async function collectUsage() {
|
|
25165
25633
|
const info = await collectAndPersist();
|
|
25634
|
+
backfillZeroCostEvents();
|
|
25635
|
+
reattributeOrphanEvents();
|
|
25166
25636
|
runRollup();
|
|
25167
25637
|
advanceMetaIso("usage_collector_heartbeat", (/* @__PURE__ */ new Date()).toISOString());
|
|
25168
25638
|
return info;
|
|
@@ -25795,6 +26265,8 @@ function createDashboardServer(options) {
|
|
|
25795
26265
|
});
|
|
25796
26266
|
}
|
|
25797
26267
|
let watcherHandle = null;
|
|
26268
|
+
const STALENESS_WATCHDOG_INTERVAL_MS = 5 * 60 * 1e3;
|
|
26269
|
+
let stalenessWatchdogTimer = null;
|
|
25798
26270
|
return {
|
|
25799
26271
|
async start() {
|
|
25800
26272
|
const { recomputeAndWrite: recomputeAndWrite2, recomputeAll: recomputeAll2, resolveDeriveContext: resolveDeriveContext2, isDeriveMigrated: isDeriveMigrated2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
|
|
@@ -25891,6 +26363,38 @@ function createDashboardServer(options) {
|
|
|
25891
26363
|
onAgentSessionsChanged: () => broadcast({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() })
|
|
25892
26364
|
});
|
|
25893
26365
|
startUsageCollector();
|
|
26366
|
+
const startupConfig = await readConfig();
|
|
26367
|
+
if (startupConfig.stalenessWatchdog) {
|
|
26368
|
+
const { collectStaleCandidates: collectStaleCandidates2 } = await Promise.resolve().then(() => (init_api(), api_exports));
|
|
26369
|
+
const { runStalenessWatchdogTick: runStalenessWatchdogTick2 } = await Promise.resolve().then(() => (init_watchdog(), watchdog_exports));
|
|
26370
|
+
const { emitEvent: emitEvent2 } = await Promise.resolve().then(() => (init_event_emit(), event_emit_exports));
|
|
26371
|
+
const stalenessSeen = /* @__PURE__ */ new Set();
|
|
26372
|
+
const watchdogTick = async () => {
|
|
26373
|
+
if (!await migrationGate()) return;
|
|
26374
|
+
try {
|
|
26375
|
+
const candidates = await collectStaleCandidates2(projectsDir, assignmentsDir2);
|
|
26376
|
+
const summary = runStalenessWatchdogTick2(candidates, stalenessSeen, (e) => {
|
|
26377
|
+
emitEvent2({
|
|
26378
|
+
assignmentId: e.assignmentId,
|
|
26379
|
+
projectSlug: e.projectSlug,
|
|
26380
|
+
type: e.type,
|
|
26381
|
+
actor: "system",
|
|
26382
|
+
details: { reasons: e.reasons.map((r) => r.kind) }
|
|
26383
|
+
});
|
|
26384
|
+
});
|
|
26385
|
+
if (summary.newlyStale > 0 || summary.cleared > 0) {
|
|
26386
|
+
console.log(
|
|
26387
|
+
`staleness watchdog: ${summary.newlyStale} newly stale, ${summary.cleared} cleared (${summary.stale}/${summary.scanned} stale).`
|
|
26388
|
+
);
|
|
26389
|
+
}
|
|
26390
|
+
} catch (err) {
|
|
26391
|
+
console.error("staleness watchdog tick failed:", err);
|
|
26392
|
+
}
|
|
26393
|
+
};
|
|
26394
|
+
void watchdogTick();
|
|
26395
|
+
stalenessWatchdogTimer = setInterval(() => void watchdogTick(), STALENESS_WATCHDOG_INTERVAL_MS);
|
|
26396
|
+
stalenessWatchdogTimer.unref?.();
|
|
26397
|
+
}
|
|
25894
26398
|
return new Promise((resolvePromise, reject) => {
|
|
25895
26399
|
server.on("error", (err) => {
|
|
25896
26400
|
if (err.code === "EADDRINUSE") {
|
|
@@ -25912,6 +26416,10 @@ function createDashboardServer(options) {
|
|
|
25912
26416
|
});
|
|
25913
26417
|
},
|
|
25914
26418
|
async stop() {
|
|
26419
|
+
if (stalenessWatchdogTimer) {
|
|
26420
|
+
clearInterval(stalenessWatchdogTimer);
|
|
26421
|
+
stalenessWatchdogTimer = null;
|
|
26422
|
+
}
|
|
25915
26423
|
await stopAutodiscovery();
|
|
25916
26424
|
await stopUsageCollector();
|
|
25917
26425
|
if (watcherHandle) {
|