@agents-tower/core 0.1.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/adapters/claude.d.ts +2 -0
- package/dist/adapters/claude.js +43 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex-cloud.d.ts +4 -0
- package/dist/adapters/codex-cloud.js +80 -0
- package/dist/adapters/codex-cloud.js.map +1 -0
- package/dist/adapters/codex-local.d.ts +17 -0
- package/dist/adapters/codex-local.js +180 -0
- package/dist/adapters/codex-local.js.map +1 -0
- package/dist/adapters/cursor-cloud.d.ts +2 -0
- package/dist/adapters/cursor-cloud.js +54 -0
- package/dist/adapters/cursor-cloud.js.map +1 -0
- package/dist/adapters/cursor-local.d.ts +2 -0
- package/dist/adapters/cursor-local.js +37 -0
- package/dist/adapters/cursor-local.js.map +1 -0
- package/dist/adapters/helpers.d.ts +15 -0
- package/dist/adapters/helpers.js +33 -0
- package/dist/adapters/helpers.js.map +1 -0
- package/dist/adapters/hermes.d.ts +2 -0
- package/dist/adapters/hermes.js +41 -0
- package/dist/adapters/hermes.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +37 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openclaw.d.ts +2 -0
- package/dist/adapters/openclaw.js +41 -0
- package/dist/adapters/openclaw.js.map +1 -0
- package/dist/adapters/presence.d.ts +2 -0
- package/dist/adapters/presence.js +36 -0
- package/dist/adapters/presence.js.map +1 -0
- package/dist/adapters/static-source.d.ts +15 -0
- package/dist/adapters/static-source.js +41 -0
- package/dist/adapters/static-source.js.map +1 -0
- package/dist/adapters/types.d.ts +44 -0
- package/dist/adapters/types.js +3 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/app-server.d.ts +94 -0
- package/dist/app-server.js +382 -0
- package/dist/app-server.js.map +1 -0
- package/dist/app-settings.d.ts +52 -0
- package/dist/app-settings.js +268 -0
- package/dist/app-settings.js.map +1 -0
- package/dist/appearance.d.ts +6 -0
- package/dist/appearance.js +84 -0
- package/dist/appearance.js.map +1 -0
- package/dist/aseprite.d.ts +4 -0
- package/dist/aseprite.js +42 -0
- package/dist/aseprite.js.map +1 -0
- package/dist/claude-agent-sdk.d.ts +44 -0
- package/dist/claude-agent-sdk.js +347 -0
- package/dist/claude-agent-sdk.js.map +1 -0
- package/dist/claude-home-cache.d.ts +61 -0
- package/dist/claude-home-cache.js +438 -0
- package/dist/claude-home-cache.js.map +1 -0
- package/dist/claude.d.ts +154 -0
- package/dist/claude.js +3495 -0
- package/dist/claude.js.map +1 -0
- package/dist/cloud.d.ts +2 -0
- package/dist/cloud.js +25 -0
- package/dist/cloud.js.map +1 -0
- package/dist/codex-command.d.ts +25 -0
- package/dist/codex-command.js +235 -0
- package/dist/codex-command.js.map +1 -0
- package/dist/codex-session-files.d.ts +8 -0
- package/dist/codex-session-files.js +509 -0
- package/dist/codex-session-files.js.map +1 -0
- package/dist/codex-thread-query.d.ts +14 -0
- package/dist/codex-thread-query.js +82 -0
- package/dist/codex-thread-query.js.map +1 -0
- package/dist/cursor-cloud-data.d.ts +34 -0
- package/dist/cursor-cloud-data.js +344 -0
- package/dist/cursor-cloud-data.js.map +1 -0
- package/dist/cursor-lib/local-discovery.d.ts +17 -0
- package/dist/cursor-lib/local-discovery.js +251 -0
- package/dist/cursor-lib/local-discovery.js.map +1 -0
- package/dist/cursor-lib/shared.d.ts +5 -0
- package/dist/cursor-lib/shared.js +88 -0
- package/dist/cursor-lib/shared.js.map +1 -0
- package/dist/cursor.d.ts +8 -0
- package/dist/cursor.js +1584 -0
- package/dist/cursor.js.map +1 -0
- package/dist/domain/codex-turn-semantics.d.ts +4 -0
- package/dist/domain/codex-turn-semantics.js +49 -0
- package/dist/domain/codex-turn-semantics.js.map +1 -0
- package/dist/domain/workload-policy.d.ts +8 -0
- package/dist/domain/workload-policy.js +268 -0
- package/dist/domain/workload-policy.js.map +1 -0
- package/dist/domain/workspace-activity.d.ts +10 -0
- package/dist/domain/workspace-activity.js +372 -0
- package/dist/domain/workspace-activity.js.map +1 -0
- package/dist/goal.d.ts +11 -0
- package/dist/goal.js +76 -0
- package/dist/goal.js.map +1 -0
- package/dist/hermes-hook-install.d.ts +12 -0
- package/dist/hermes-hook-install.js +335 -0
- package/dist/hermes-hook-install.js.map +1 -0
- package/dist/hermes.d.ts +97 -0
- package/dist/hermes.js +2446 -0
- package/dist/hermes.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/live-monitor-lib/events.d.ts +43 -0
- package/dist/live-monitor-lib/events.js +1463 -0
- package/dist/live-monitor-lib/events.js.map +1 -0
- package/dist/live-monitor-lib/rollout-hooks.d.ts +13 -0
- package/dist/live-monitor-lib/rollout-hooks.js +253 -0
- package/dist/live-monitor-lib/rollout-hooks.js.map +1 -0
- package/dist/live-monitor.d.ts +102 -0
- package/dist/live-monitor.js +1461 -0
- package/dist/live-monitor.js.map +1 -0
- package/dist/local-thread-selection.d.ts +2 -0
- package/dist/local-thread-selection.js +58 -0
- package/dist/local-thread-selection.js.map +1 -0
- package/dist/openclaw.d.ts +41 -0
- package/dist/openclaw.js +715 -0
- package/dist/openclaw.js.map +1 -0
- package/dist/presence.d.ts +9 -0
- package/dist/presence.js +108 -0
- package/dist/presence.js.map +1 -0
- package/dist/project-identity.d.ts +7 -0
- package/dist/project-identity.js +162 -0
- package/dist/project-identity.js.map +1 -0
- package/dist/project-paths.d.ts +23 -0
- package/dist/project-paths.js +394 -0
- package/dist/project-paths.js.map +1 -0
- package/dist/project-storage.d.ts +7 -0
- package/dist/project-storage.js +79 -0
- package/dist/project-storage.js.map +1 -0
- package/dist/room-config.d.ts +21 -0
- package/dist/room-config.js +230 -0
- package/dist/room-config.js.map +1 -0
- package/dist/services/project-discovery.d.ts +1 -0
- package/dist/services/project-discovery.js +16 -0
- package/dist/services/project-discovery.js.map +1 -0
- package/dist/services/project-live-monitor.d.ts +1 -0
- package/dist/services/project-live-monitor.js +6 -0
- package/dist/services/project-live-monitor.js.map +1 -0
- package/dist/services/project-snapshot-coordinator.d.ts +49 -0
- package/dist/services/project-snapshot-coordinator.js +152 -0
- package/dist/services/project-snapshot-coordinator.js.map +1 -0
- package/dist/services/refresh-scheduler.d.ts +8 -0
- package/dist/services/refresh-scheduler.js +44 -0
- package/dist/services/refresh-scheduler.js.map +1 -0
- package/dist/services/snapshot-assembler.d.ts +8 -0
- package/dist/services/snapshot-assembler.js +167 -0
- package/dist/services/snapshot-assembler.js.map +1 -0
- package/dist/services/thread-store.d.ts +4 -0
- package/dist/services/thread-store.js +8 -0
- package/dist/services/thread-store.js.map +1 -0
- package/dist/snapshot-lib/activity-summary.d.ts +3 -0
- package/dist/snapshot-lib/activity-summary.js +220 -0
- package/dist/snapshot-lib/activity-summary.js.map +1 -0
- package/dist/snapshot-lib/dashboard-builder.d.ts +21 -0
- package/dist/snapshot-lib/dashboard-builder.js +146 -0
- package/dist/snapshot-lib/dashboard-builder.js.map +1 -0
- package/dist/snapshot-lib/thread-summary.d.ts +27 -0
- package/dist/snapshot-lib/thread-summary.js +953 -0
- package/dist/snapshot-lib/thread-summary.js.map +1 -0
- package/dist/snapshot.d.ts +2 -0
- package/dist/snapshot.js +21 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/types.d.ts +368 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/json.d.ts +4 -0
- package/dist/utils/json.js +29 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/text.d.ts +3 -0
- package/dist/utils/text.js +26 -0
- package/dist/utils/text.js.map +1 -0
- package/dist/web-search.d.ts +1 -0
- package/dist/web-search.js +52 -0
- package/dist/web-search.js.map +1 -0
- package/dist/workload.d.ts +1 -0
- package/dist/workload.js +12 -0
- package/dist/workload.js.map +1 -0
- package/package.json +19 -0
|
@@ -0,0 +1,1461 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ProjectLiveMonitor = exports.shouldMarkThreadStoppedFromAppServerNotification = exports.shouldMarkThreadLiveFromAppServerNotification = exports.buildRolloutHookEvent = exports.parseApplyPatchInput = exports.hasEquivalentRecentMessageEvent = exports.extractCollabReceiverThreadIds = exports.buildThreadReadAgentMessageEvent = exports.buildDashboardEventFromAppServerMessage = exports.buildAppServerDiagnosticNote = void 0;
|
|
37
|
+
const node_events_1 = require("node:events");
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const app_server_1 = require("./app-server");
|
|
40
|
+
const codex_thread_query_1 = require("./codex-thread-query");
|
|
41
|
+
const cloud_1 = require("./cloud");
|
|
42
|
+
const goal_1 = require("./goal");
|
|
43
|
+
const project_paths_1 = require("./project-paths");
|
|
44
|
+
const codex_turn_semantics_1 = require("./domain/codex-turn-semantics");
|
|
45
|
+
const room_config_1 = require("./room-config");
|
|
46
|
+
const snapshot_1 = require("./snapshot");
|
|
47
|
+
const events_1 = require("./live-monitor-lib/events");
|
|
48
|
+
Object.defineProperty(exports, "buildAppServerDiagnosticNote", { enumerable: true, get: function () { return events_1.buildAppServerDiagnosticNote; } });
|
|
49
|
+
Object.defineProperty(exports, "buildDashboardEventFromAppServerMessage", { enumerable: true, get: function () { return events_1.buildDashboardEventFromAppServerMessage; } });
|
|
50
|
+
Object.defineProperty(exports, "buildThreadReadAgentMessageEvent", { enumerable: true, get: function () { return events_1.buildThreadReadAgentMessageEvent; } });
|
|
51
|
+
Object.defineProperty(exports, "extractCollabReceiverThreadIds", { enumerable: true, get: function () { return events_1.extractCollabReceiverThreadIds; } });
|
|
52
|
+
Object.defineProperty(exports, "hasEquivalentRecentMessageEvent", { enumerable: true, get: function () { return events_1.hasEquivalentRecentMessageEvent; } });
|
|
53
|
+
Object.defineProperty(exports, "shouldMarkThreadLiveFromAppServerNotification", { enumerable: true, get: function () { return events_1.shouldMarkThreadLiveFromAppServerNotification; } });
|
|
54
|
+
Object.defineProperty(exports, "shouldMarkThreadStoppedFromAppServerNotification", { enumerable: true, get: function () { return events_1.shouldMarkThreadStoppedFromAppServerNotification; } });
|
|
55
|
+
const rollout_hooks_1 = require("./live-monitor-lib/rollout-hooks");
|
|
56
|
+
Object.defineProperty(exports, "buildRolloutHookEvent", { enumerable: true, get: function () { return rollout_hooks_1.buildRolloutHookEvent; } });
|
|
57
|
+
Object.defineProperty(exports, "parseApplyPatchInput", { enumerable: true, get: function () { return rollout_hooks_1.parseApplyPatchInput; } });
|
|
58
|
+
const workload_1 = require("./workload");
|
|
59
|
+
const DISCOVERY_INTERVAL_MS = 4000;
|
|
60
|
+
const CLOUD_REFRESH_INTERVAL_MS = 30000;
|
|
61
|
+
const FILE_WATCH_INTERVAL_MS = 250;
|
|
62
|
+
const SNAPSHOT_DEBOUNCE_MS = 60;
|
|
63
|
+
const THREAD_READ_DEBOUNCE_MS = 80;
|
|
64
|
+
const ACTIVE_SUBSCRIPTION_WINDOW_MS = 10 * 60 * 1000;
|
|
65
|
+
const MAX_SUBSCRIBED_THREADS = 8;
|
|
66
|
+
const MAX_RECENT_EVENTS = 240;
|
|
67
|
+
const RECENT_EVENT_RETENTION_MS = 45 * 60 * 1000;
|
|
68
|
+
// Desktop-backed threads can take noticeably longer to resume than simple CLI
|
|
69
|
+
// sessions, and a too-short timeout drops the live item stream we need for
|
|
70
|
+
// `item/agentMessage/*` notifications.
|
|
71
|
+
// Desktop-backed thread attaches can easily take 20s+ on large rollouts.
|
|
72
|
+
// Keep a wider budget so live subscriptions don't flap back to read-only.
|
|
73
|
+
const APP_SERVER_SUBSCRIPTION_TIMEOUT_MS = 60000;
|
|
74
|
+
const APP_SERVER_DISCOVERY_REQUEST_TIMEOUT_MS = 15000;
|
|
75
|
+
const CLOUD_NOTE_LEGACY_PREFIX = "Codex cloud list unavailable:";
|
|
76
|
+
const CLOUD_NOTE_PREFIX = "Codex cloud ";
|
|
77
|
+
const STOPPED_THREAD_REMOVAL_BUFFER_MS = 1000;
|
|
78
|
+
const NOT_LOADED_STOP_DEBOUNCE_MS = 3000;
|
|
79
|
+
const DEFAULT_LOCAL_THREAD_LIMIT = 24;
|
|
80
|
+
const APPROVAL_DECISIONS = new Set(["accept", "acceptForSession", "decline", "cancel"]);
|
|
81
|
+
async function withTimeout(promise, timeoutMs, label) {
|
|
82
|
+
return await Promise.race([
|
|
83
|
+
promise,
|
|
84
|
+
new Promise((_, reject) => {
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
87
|
+
}, timeoutMs);
|
|
88
|
+
timer.unref?.();
|
|
89
|
+
promise.finally(() => clearTimeout(timer)).catch(() => clearTimeout(timer));
|
|
90
|
+
})
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
function latestInProgressTurn(thread) {
|
|
94
|
+
return [...thread.turns].reverse().find((turn) => turn.status === "inProgress") ?? null;
|
|
95
|
+
}
|
|
96
|
+
function notificationTurn(params) {
|
|
97
|
+
const turn = (0, events_1.asRecord)((0, events_1.asRecord)(params)?.turn);
|
|
98
|
+
const id = (0, events_1.asString)(turn?.id);
|
|
99
|
+
const status = (0, events_1.asString)(turn?.status);
|
|
100
|
+
if (!id || !status || !["completed", "interrupted", "failed", "inProgress"].includes(status)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
status: status,
|
|
106
|
+
error: (0, events_1.asRecord)(turn?.error),
|
|
107
|
+
items: Array.isArray(turn?.items) ? turn.items : []
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function childThreadsByParent(threads) {
|
|
111
|
+
const byParent = new Map();
|
|
112
|
+
for (const thread of threads) {
|
|
113
|
+
const parentThreadId = (0, snapshot_1.parentThreadIdForThread)(thread);
|
|
114
|
+
if (!parentThreadId) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const children = byParent.get(parentThreadId) ?? [];
|
|
118
|
+
children.push(thread);
|
|
119
|
+
byParent.set(parentThreadId, children);
|
|
120
|
+
}
|
|
121
|
+
return byParent;
|
|
122
|
+
}
|
|
123
|
+
function upsertThreadTurn(thread, turn) {
|
|
124
|
+
const turns = thread.turns.filter((entry) => entry.id !== turn.id);
|
|
125
|
+
turns.push({
|
|
126
|
+
...turn,
|
|
127
|
+
items: turn.items.length > 0
|
|
128
|
+
? turn.items
|
|
129
|
+
: thread.turns.find((entry) => entry.id === turn.id)?.items ?? []
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
...thread,
|
|
133
|
+
turns
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function threadStillAwaitsFinalAnswer(thread) {
|
|
137
|
+
if (thread.status.type === "systemError") {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const lastTurn = thread.turns.at(-1);
|
|
141
|
+
if (!lastTurn || lastTurn.status === "failed" || (0, codex_turn_semantics_1.turnHasFinalAnswer)(lastTurn)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return lastTurn.status === "inProgress" || (0, codex_turn_semantics_1.turnHasNonFinalWorkSignal)(lastTurn);
|
|
145
|
+
}
|
|
146
|
+
function isSettledDormantSubagentThread(thread) {
|
|
147
|
+
if (!(0, snapshot_1.parentThreadIdForThread)(thread)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (thread.status.type !== "idle" && thread.status.type !== "notLoaded") {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const lastTurn = thread.turns.at(-1);
|
|
154
|
+
if (!lastTurn || lastTurn.status === "inProgress") {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (thread.status.type === "notLoaded" && !(0, codex_turn_semantics_1.turnHasOpenWorkSignal)(lastTurn)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
const summary = (0, snapshot_1.summariseThread)(thread);
|
|
161
|
+
return summary.state === "done" || summary.state === "idle";
|
|
162
|
+
}
|
|
163
|
+
function isFreshEnoughToPromoteAwaitingFinalAnswer(thread) {
|
|
164
|
+
const updatedAtMs = thread.updatedAt * 1000;
|
|
165
|
+
return Number.isFinite(updatedAtMs) && Date.now() - updatedAtMs <= ACTIVE_SUBSCRIPTION_WINDOW_MS;
|
|
166
|
+
}
|
|
167
|
+
function isRecentThreadSubscriptionCandidate(thread, now) {
|
|
168
|
+
if ((0, snapshot_1.parentThreadIdForThread)(thread)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const updatedAtMs = thread.updatedAt * 1000;
|
|
172
|
+
return Number.isFinite(updatedAtMs) && now - updatedAtMs <= ACTIVE_SUBSCRIPTION_WINDOW_MS;
|
|
173
|
+
}
|
|
174
|
+
function shouldPreserveKnownOngoingThread(thread, wasOngoing) {
|
|
175
|
+
if ((0, snapshot_1.isOngoingThread)(thread)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (!wasOngoing || isSettledDormantSubagentThread(thread)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return threadStillAwaitsFinalAnswer(thread);
|
|
182
|
+
}
|
|
183
|
+
class ProjectLiveMonitor extends node_events_1.EventEmitter {
|
|
184
|
+
projectRoot;
|
|
185
|
+
localLimit;
|
|
186
|
+
includeCloud;
|
|
187
|
+
appServerRequestTimeoutMs;
|
|
188
|
+
threads = new Map();
|
|
189
|
+
threadWatchers = new Map();
|
|
190
|
+
threadReadTimers = new Map();
|
|
191
|
+
threadRefreshGenerations = new Map();
|
|
192
|
+
threadPaths = new Map();
|
|
193
|
+
notes = new Set();
|
|
194
|
+
roomConfigPath;
|
|
195
|
+
pendingUserRequests = new Map();
|
|
196
|
+
subscribedThreadIds = new Set();
|
|
197
|
+
ongoingThreadIds = new Set();
|
|
198
|
+
stoppedAtByThreadId = new Map();
|
|
199
|
+
hydratedThreadIds = new Set();
|
|
200
|
+
threadGoals = new Map();
|
|
201
|
+
threadRemovalTimers = new Map();
|
|
202
|
+
pendingNotLoadedStopTimers = new Map();
|
|
203
|
+
roomWatcher = null;
|
|
204
|
+
snapshot = null;
|
|
205
|
+
snapshotCoordinator = null;
|
|
206
|
+
snapshotCoordinatorPromise = null;
|
|
207
|
+
cloudTasks = [];
|
|
208
|
+
client = null;
|
|
209
|
+
discoveryTimer = null;
|
|
210
|
+
cloudTimer = null;
|
|
211
|
+
snapshotTimer = null;
|
|
212
|
+
discoveryPromise = null;
|
|
213
|
+
discoveryRequested = 0;
|
|
214
|
+
discoveryCompleted = 0;
|
|
215
|
+
snapshotRebuildPromise = null;
|
|
216
|
+
snapshotRebuildRequested = 0;
|
|
217
|
+
snapshotRebuildCompleted = 0;
|
|
218
|
+
stopping = false;
|
|
219
|
+
stopped = false;
|
|
220
|
+
stopPromise = null;
|
|
221
|
+
recentEvents = [];
|
|
222
|
+
unsubscribeNotifications = null;
|
|
223
|
+
unsubscribeServerRequests = null;
|
|
224
|
+
subscriptionSyncPromise = null;
|
|
225
|
+
subscriptionSyncQueued = false;
|
|
226
|
+
constructor(options) {
|
|
227
|
+
super();
|
|
228
|
+
this.projectRoot = options.projectRoot;
|
|
229
|
+
this.localLimit = options.localLimit ?? DEFAULT_LOCAL_THREAD_LIMIT;
|
|
230
|
+
this.includeCloud = options.includeCloud !== false;
|
|
231
|
+
this.appServerRequestTimeoutMs = options.appServerRequestTimeoutMs
|
|
232
|
+
?? APP_SERVER_DISCOVERY_REQUEST_TIMEOUT_MS;
|
|
233
|
+
this.roomConfigPath = (0, room_config_1.getRoomsFilePath)(this.projectRoot);
|
|
234
|
+
}
|
|
235
|
+
getSnapshot() {
|
|
236
|
+
return this.snapshot;
|
|
237
|
+
}
|
|
238
|
+
async start() {
|
|
239
|
+
const coordinator = await this.ensureSnapshotCoordinator();
|
|
240
|
+
await Promise.all([this.ensureClient(), coordinator.warm()]);
|
|
241
|
+
await this.discoverThreads();
|
|
242
|
+
this.roomConfigPath = await (0, room_config_1.resolveReadableRoomsFilePath)(this.projectRoot);
|
|
243
|
+
this.watchRoomsFile();
|
|
244
|
+
this.discoveryTimer = setInterval(() => {
|
|
245
|
+
void this.discoverThreads();
|
|
246
|
+
void this.refreshSecondarySources();
|
|
247
|
+
}, DISCOVERY_INTERVAL_MS);
|
|
248
|
+
if (this.includeCloud) {
|
|
249
|
+
void this.refreshCloudTasks();
|
|
250
|
+
this.cloudTimer = setInterval(() => {
|
|
251
|
+
void this.refreshCloudTasks();
|
|
252
|
+
}, CLOUD_REFRESH_INTERVAL_MS);
|
|
253
|
+
}
|
|
254
|
+
await this.rebuildSnapshot();
|
|
255
|
+
}
|
|
256
|
+
async refreshNow() {
|
|
257
|
+
await Promise.all([
|
|
258
|
+
this.includeCloud ? this.refreshCloudTasks() : Promise.resolve(),
|
|
259
|
+
this.discoverThreads(),
|
|
260
|
+
this.refreshSecondarySources("manual", 0)
|
|
261
|
+
]);
|
|
262
|
+
await this.rebuildSnapshot();
|
|
263
|
+
}
|
|
264
|
+
async respondToApprovalRequest(requestId, decision) {
|
|
265
|
+
if (!APPROVAL_DECISIONS.has(decision)) {
|
|
266
|
+
throw new Error(`Unsupported approval decision: ${decision}`);
|
|
267
|
+
}
|
|
268
|
+
await this.ensureClient();
|
|
269
|
+
if (!this.client) {
|
|
270
|
+
throw new Error("Local Codex app-server unavailable.");
|
|
271
|
+
}
|
|
272
|
+
const pending = this.pendingUserRequests.get(requestId) ?? null;
|
|
273
|
+
if (!pending) {
|
|
274
|
+
throw new Error("Approval request not found or already resolved.");
|
|
275
|
+
}
|
|
276
|
+
if (pending.kind !== "approval") {
|
|
277
|
+
throw new Error("Only approval requests are actionable from Agents Office right now.");
|
|
278
|
+
}
|
|
279
|
+
const numericRequestId = Number.parseInt(requestId, 10);
|
|
280
|
+
if (!Number.isFinite(numericRequestId)) {
|
|
281
|
+
throw new Error(`Invalid approval request id: ${requestId}`);
|
|
282
|
+
}
|
|
283
|
+
if (pending.responseKind === "legacyReview") {
|
|
284
|
+
this.client.respondToServerRequest(numericRequestId, {
|
|
285
|
+
decision: legacyReviewDecision(decision)
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
else if (pending.responseKind === "permissionsApproval") {
|
|
289
|
+
if (decision !== "accept" && decision !== "acceptForSession") {
|
|
290
|
+
throw new Error("Permission-profile requests only support accept or acceptForSession from Agents Office.");
|
|
291
|
+
}
|
|
292
|
+
this.client.respondToServerRequest(numericRequestId, {
|
|
293
|
+
permissions: pending.requestedPermissions ?? { network: null, fileSystem: null },
|
|
294
|
+
scope: decision === "acceptForSession" ? "session" : "turn"
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this.client.respondToApprovalRequest(numericRequestId, decision);
|
|
299
|
+
}
|
|
300
|
+
this.pendingUserRequests.delete(requestId);
|
|
301
|
+
this.markThreadLive(pending.threadId);
|
|
302
|
+
this.scheduleThreadRefresh(pending.threadId);
|
|
303
|
+
await this.rebuildSnapshot();
|
|
304
|
+
}
|
|
305
|
+
async sendThreadReply(threadId, text) {
|
|
306
|
+
const normalizedText = text.trim();
|
|
307
|
+
if (!normalizedText) {
|
|
308
|
+
throw new Error("Reply text is required.");
|
|
309
|
+
}
|
|
310
|
+
await this.ensureClient();
|
|
311
|
+
if (!this.client) {
|
|
312
|
+
throw new Error("Local Codex app-server unavailable.");
|
|
313
|
+
}
|
|
314
|
+
let thread = this.threads.get(threadId) ?? null;
|
|
315
|
+
if (!thread || thread.status.type === "notLoaded") {
|
|
316
|
+
thread = await this.client.resumeThread(threadId);
|
|
317
|
+
this.threads.set(threadId, thread);
|
|
318
|
+
}
|
|
319
|
+
const threadProjectRoot = (0, project_paths_1.canonicalizeProjectPath)(thread.cwd) ?? thread.cwd;
|
|
320
|
+
if (!(threadProjectRoot === this.projectRoot || threadProjectRoot.startsWith(`${this.projectRoot}/`))) {
|
|
321
|
+
throw new Error("Thread does not belong to this project.");
|
|
322
|
+
}
|
|
323
|
+
const sourceKind = (0, snapshot_1.parseThreadSourceMeta)(thread).sourceKind;
|
|
324
|
+
if (sourceKind !== "appServer") {
|
|
325
|
+
throw new Error("Browser replies are only supported for Codex app-server-owned threads. Continue this observed desktop/CLI thread in Codex instead.");
|
|
326
|
+
}
|
|
327
|
+
let activeTurn = latestInProgressTurn(thread);
|
|
328
|
+
if (!activeTurn && thread.status.type === "active") {
|
|
329
|
+
thread = await this.client.readThread(threadId);
|
|
330
|
+
this.threads.set(threadId, thread);
|
|
331
|
+
activeTurn = latestInProgressTurn(thread);
|
|
332
|
+
}
|
|
333
|
+
if (activeTurn?.id) {
|
|
334
|
+
this.client.steerTurnNoWait(threadId, activeTurn.id, normalizedText);
|
|
335
|
+
}
|
|
336
|
+
else if (thread.status.type === "active") {
|
|
337
|
+
throw new Error("Active Codex thread has no steerable turn yet. Wait for the live turn to load, then retry.");
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
this.client.startTurnNoWait(threadId, normalizedText, thread.cwd);
|
|
341
|
+
}
|
|
342
|
+
this.markThreadLive(threadId);
|
|
343
|
+
this.scheduleThreadRefresh(threadId);
|
|
344
|
+
this.scheduleThreadSubscriptions();
|
|
345
|
+
await this.rebuildSnapshot();
|
|
346
|
+
}
|
|
347
|
+
async respondToInputRequest(requestId, answers) {
|
|
348
|
+
await this.ensureClient();
|
|
349
|
+
if (!this.client) {
|
|
350
|
+
throw new Error("Local Codex app-server unavailable.");
|
|
351
|
+
}
|
|
352
|
+
const pending = this.pendingUserRequests.get(requestId) ?? null;
|
|
353
|
+
if (!pending) {
|
|
354
|
+
throw new Error("Input request not found or already resolved.");
|
|
355
|
+
}
|
|
356
|
+
if (pending.kind !== "input") {
|
|
357
|
+
throw new Error("Only input requests can be answered with browser input.");
|
|
358
|
+
}
|
|
359
|
+
const normalizedAnswers = normalizeUserInputAnswers(pending.questions ?? [], answers);
|
|
360
|
+
const numericRequestId = Number.parseInt(requestId, 10);
|
|
361
|
+
if (!Number.isFinite(numericRequestId)) {
|
|
362
|
+
throw new Error(`Invalid input request id: ${requestId}`);
|
|
363
|
+
}
|
|
364
|
+
if (pending.responseKind === "mcpElicitation") {
|
|
365
|
+
this.client.respondToServerRequest(numericRequestId, {
|
|
366
|
+
action: "accept",
|
|
367
|
+
content: normalizeMcpElicitationContent(pending.requestedSchema ?? null, normalizedAnswers),
|
|
368
|
+
_meta: null
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
this.client.respondToToolRequestUserInput(numericRequestId, {
|
|
373
|
+
answers: normalizedAnswers
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
this.pendingUserRequests.delete(requestId);
|
|
377
|
+
this.markThreadLive(pending.threadId);
|
|
378
|
+
this.scheduleThreadRefresh(pending.threadId);
|
|
379
|
+
await this.rebuildSnapshot();
|
|
380
|
+
}
|
|
381
|
+
setSharedCloudTasks(tasks, errorMessage) {
|
|
382
|
+
this.cloudTasks = (0, snapshot_1.filterProjectCloudTasks)(tasks, this.projectRoot);
|
|
383
|
+
this.setCloudErrorNote(errorMessage);
|
|
384
|
+
this.scheduleSnapshot();
|
|
385
|
+
}
|
|
386
|
+
stop() {
|
|
387
|
+
if (!this.stopPromise) {
|
|
388
|
+
this.stopPromise = this.performStop();
|
|
389
|
+
}
|
|
390
|
+
return this.stopPromise;
|
|
391
|
+
}
|
|
392
|
+
async performStop() {
|
|
393
|
+
this.stopping = true;
|
|
394
|
+
if (this.discoveryTimer) {
|
|
395
|
+
clearInterval(this.discoveryTimer);
|
|
396
|
+
}
|
|
397
|
+
if (this.cloudTimer) {
|
|
398
|
+
clearInterval(this.cloudTimer);
|
|
399
|
+
}
|
|
400
|
+
if (this.snapshotTimer) {
|
|
401
|
+
clearTimeout(this.snapshotTimer);
|
|
402
|
+
}
|
|
403
|
+
if (this.roomWatcher) {
|
|
404
|
+
this.roomWatcher.close();
|
|
405
|
+
}
|
|
406
|
+
(0, node_fs_1.unwatchFile)(this.roomConfigPath);
|
|
407
|
+
for (const timer of this.threadReadTimers.values()) {
|
|
408
|
+
clearTimeout(timer);
|
|
409
|
+
}
|
|
410
|
+
this.threadReadTimers.clear();
|
|
411
|
+
this.threadRefreshGenerations.clear();
|
|
412
|
+
for (const timer of this.threadRemovalTimers.values()) {
|
|
413
|
+
clearTimeout(timer);
|
|
414
|
+
}
|
|
415
|
+
this.threadRemovalTimers.clear();
|
|
416
|
+
for (const timer of this.pendingNotLoadedStopTimers.values()) {
|
|
417
|
+
clearTimeout(timer);
|
|
418
|
+
}
|
|
419
|
+
this.pendingNotLoadedStopTimers.clear();
|
|
420
|
+
for (const [threadId, watcher] of this.threadWatchers.entries()) {
|
|
421
|
+
watcher.close();
|
|
422
|
+
const path = this.threadPaths.get(threadId);
|
|
423
|
+
if (path) {
|
|
424
|
+
(0, node_fs_1.unwatchFile)(path);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
this.threadWatchers.clear();
|
|
428
|
+
this.threadPaths.clear();
|
|
429
|
+
this.unsubscribeNotifications?.();
|
|
430
|
+
this.unsubscribeNotifications = null;
|
|
431
|
+
this.unsubscribeServerRequests?.();
|
|
432
|
+
this.unsubscribeServerRequests = null;
|
|
433
|
+
this.client?.close();
|
|
434
|
+
this.client = null;
|
|
435
|
+
await this.discoveryPromise?.catch(() => undefined);
|
|
436
|
+
await this.snapshotRebuildPromise?.catch((error) => {
|
|
437
|
+
if (!this.stopping && !this.stopped) {
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
const coordinator = this.snapshotCoordinator
|
|
442
|
+
?? await this.snapshotCoordinatorPromise?.catch(() => null)
|
|
443
|
+
?? null;
|
|
444
|
+
await coordinator?.dispose();
|
|
445
|
+
this.snapshotCoordinator = null;
|
|
446
|
+
this.snapshotCoordinatorPromise = null;
|
|
447
|
+
this.ongoingThreadIds.clear();
|
|
448
|
+
this.subscribedThreadIds.clear();
|
|
449
|
+
this.stopped = true;
|
|
450
|
+
this.stopping = false;
|
|
451
|
+
}
|
|
452
|
+
async ensureClient() {
|
|
453
|
+
if (this.stopping || this.stopped) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (this.client) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
this.client = await app_server_1.CodexAppServerClient.create();
|
|
461
|
+
this.unsubscribeNotifications?.();
|
|
462
|
+
this.unsubscribeServerRequests?.();
|
|
463
|
+
this.unsubscribeNotifications = this.client.onNotification((notification) => {
|
|
464
|
+
this.handleAppServerNotification(notification);
|
|
465
|
+
});
|
|
466
|
+
this.unsubscribeServerRequests = this.client.onServerRequest((request) => {
|
|
467
|
+
this.handleAppServerServerRequest(request);
|
|
468
|
+
});
|
|
469
|
+
this.clearMatchingNote("Local Codex app-server unavailable:");
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
473
|
+
this.addNote(`Local Codex app-server unavailable: ${message}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
addNote(note) {
|
|
477
|
+
this.notes.add(note);
|
|
478
|
+
}
|
|
479
|
+
clearMatchingNote(prefix) {
|
|
480
|
+
for (const note of this.notes) {
|
|
481
|
+
if (note.startsWith(prefix)) {
|
|
482
|
+
this.notes.delete(note);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
recoverTimedOutAppServerClient(error) {
|
|
487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
+
if (!/timed out after \d+ms/i.test(message)) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
this.addNote(`Local Codex app-server connection reset after timeout: ${message}`);
|
|
492
|
+
this.client?.close();
|
|
493
|
+
this.client = null;
|
|
494
|
+
this.subscribedThreadIds.clear();
|
|
495
|
+
}
|
|
496
|
+
setCloudErrorNote(errorMessage) {
|
|
497
|
+
this.clearMatchingNote(CLOUD_NOTE_LEGACY_PREFIX);
|
|
498
|
+
this.clearMatchingNote(CLOUD_NOTE_PREFIX);
|
|
499
|
+
if (!errorMessage) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this.addNote(errorMessage.startsWith(CLOUD_NOTE_PREFIX)
|
|
503
|
+
? errorMessage
|
|
504
|
+
: `${CLOUD_NOTE_LEGACY_PREFIX} ${errorMessage}`);
|
|
505
|
+
}
|
|
506
|
+
discoverThreads() {
|
|
507
|
+
if (this.stopping || this.stopped) {
|
|
508
|
+
return Promise.resolve();
|
|
509
|
+
}
|
|
510
|
+
this.discoveryRequested += 1;
|
|
511
|
+
if (!this.discoveryPromise) {
|
|
512
|
+
const pump = this.runDiscoveryPump();
|
|
513
|
+
this.discoveryPromise = pump.finally(() => {
|
|
514
|
+
this.discoveryPromise = null;
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return this.discoveryPromise;
|
|
518
|
+
}
|
|
519
|
+
async runDiscoveryPump() {
|
|
520
|
+
while (this.discoveryCompleted < this.discoveryRequested) {
|
|
521
|
+
if (this.stopping || this.stopped) {
|
|
522
|
+
this.discoveryCompleted = this.discoveryRequested;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const targetGeneration = this.discoveryRequested;
|
|
526
|
+
await this.performThreadDiscovery();
|
|
527
|
+
this.discoveryCompleted = targetGeneration;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async performThreadDiscovery() {
|
|
531
|
+
if (this.stopping || this.stopped) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
await this.ensureClient();
|
|
535
|
+
if (!this.client) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
const query = await (0, codex_thread_query_1.listCodexProjectThreadCandidates)({
|
|
540
|
+
client: this.client,
|
|
541
|
+
projectRoot: this.projectRoot,
|
|
542
|
+
localLimit: this.localLimit
|
|
543
|
+
});
|
|
544
|
+
if (this.stopping || this.stopped) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (query.usedUnscopedFallback) {
|
|
548
|
+
this.addNote("Local Codex cwd filter returned no project threads; used unscoped Windows path fallback.");
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
this.clearMatchingNote("Local Codex cwd filter returned no project threads;");
|
|
552
|
+
}
|
|
553
|
+
const projectThreads = query.projectThreads;
|
|
554
|
+
const projectThreadsById = new Map(projectThreads.map((thread) => [thread.id, thread]));
|
|
555
|
+
const trackedThreads = new Map(query.trackedThreads
|
|
556
|
+
.map((thread) => [thread.id, thread]));
|
|
557
|
+
const listedThreadsById = new Map(query.allThreads.map((thread) => [thread.id, thread]));
|
|
558
|
+
const listedChildrenByParent = childThreadsByParent(query.allThreads);
|
|
559
|
+
const loadedThreadIds = await withTimeout(this.client.listLoadedThreads(), this.appServerRequestTimeoutMs, "thread/loaded/list");
|
|
560
|
+
for (const threadId of loadedThreadIds) {
|
|
561
|
+
if (projectThreadsById.has(threadId) || trackedThreads.has(threadId)) {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
let loadedThread = null;
|
|
565
|
+
try {
|
|
566
|
+
loadedThread = await (0, codex_thread_query_1.readCodexThreadWithTimeout)(this.client, threadId, this.appServerRequestTimeoutMs);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
loadedThread = null;
|
|
570
|
+
}
|
|
571
|
+
if (!loadedThread) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const canonicalCwd = (0, project_paths_1.canonicalizeProjectPath)(loadedThread.cwd) ?? loadedThread.cwd;
|
|
575
|
+
if (!(canonicalCwd === this.projectRoot || canonicalCwd.startsWith(`${this.projectRoot}/`))) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
projectThreadsById.set(loadedThread.id, loadedThread);
|
|
579
|
+
trackedThreads.set(loadedThread.id, loadedThread);
|
|
580
|
+
listedThreadsById.set(loadedThread.id, loadedThread);
|
|
581
|
+
}
|
|
582
|
+
const pendingRelatedThreads = [...trackedThreads.values()];
|
|
583
|
+
while (pendingRelatedThreads.length > 0) {
|
|
584
|
+
const thread = pendingRelatedThreads.shift();
|
|
585
|
+
if (!thread) {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const parentThreadId = (0, snapshot_1.parentThreadIdForThread)(thread);
|
|
589
|
+
if (parentThreadId && !trackedThreads.has(parentThreadId)) {
|
|
590
|
+
let parentThread = projectThreadsById.get(parentThreadId)
|
|
591
|
+
?? listedThreadsById.get(parentThreadId)
|
|
592
|
+
?? this.threads.get(parentThreadId)
|
|
593
|
+
?? null;
|
|
594
|
+
if (!parentThread) {
|
|
595
|
+
try {
|
|
596
|
+
parentThread = await (0, codex_thread_query_1.readCodexThreadWithTimeout)(this.client, parentThreadId, this.appServerRequestTimeoutMs);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
parentThread = null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (parentThread) {
|
|
603
|
+
projectThreadsById.set(parentThread.id, parentThread);
|
|
604
|
+
trackedThreads.set(parentThread.id, parentThread);
|
|
605
|
+
pendingRelatedThreads.push(parentThread);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
for (const childThread of listedChildrenByParent.get(thread.id) ?? []) {
|
|
609
|
+
if (trackedThreads.has(childThread.id)) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
projectThreadsById.set(childThread.id, childThread);
|
|
613
|
+
trackedThreads.set(childThread.id, childThread);
|
|
614
|
+
pendingRelatedThreads.push(childThread);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
for (const threadId of Array.from(this.threads.keys())) {
|
|
618
|
+
if (trackedThreads.has(threadId)) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const knownThread = projectThreadsById.get(threadId);
|
|
622
|
+
if (knownThread) {
|
|
623
|
+
trackedThreads.set(threadId, knownThread);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
this.clearMatchingNote("Local Codex app-server unavailable:");
|
|
627
|
+
await Promise.all([...trackedThreads.values()].map(async (listedThread) => {
|
|
628
|
+
const known = this.threads.get(listedThread.id);
|
|
629
|
+
if (!known || known.updatedAt !== listedThread.updatedAt || known.path !== listedThread.path) {
|
|
630
|
+
await this.refreshThread(listedThread.id, listedThread);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const mergedThread = {
|
|
634
|
+
...known,
|
|
635
|
+
status: listedThread.status,
|
|
636
|
+
updatedAt: listedThread.updatedAt,
|
|
637
|
+
path: listedThread.path ?? known.path
|
|
638
|
+
};
|
|
639
|
+
if (shouldPreserveKnownOngoingThread(mergedThread, this.ongoingThreadIds.has(listedThread.id))) {
|
|
640
|
+
this.markThreadLive(listedThread.id);
|
|
641
|
+
}
|
|
642
|
+
else if (this.ongoingThreadIds.has(listedThread.id)) {
|
|
643
|
+
this.markThreadStopped(listedThread.id);
|
|
644
|
+
}
|
|
645
|
+
this.threads.set(listedThread.id, mergedThread);
|
|
646
|
+
await this.refreshThreadGoal(listedThread.id);
|
|
647
|
+
this.ensureThreadWatcher(listedThread.id, listedThread.path ?? known.path);
|
|
648
|
+
}));
|
|
649
|
+
for (const threadId of Array.from(this.threads.keys())) {
|
|
650
|
+
if (!projectThreadsById.has(threadId)) {
|
|
651
|
+
if (this.ongoingThreadIds.has(threadId)) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
this.markThreadStopped(threadId);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
this.scheduleThreadSubscriptions();
|
|
658
|
+
this.scheduleSnapshot();
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
if (this.stopping || this.stopped) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
665
|
+
this.addNote(`Local Codex app-server unavailable: ${message}`);
|
|
666
|
+
this.recoverTimedOutAppServerClient(error);
|
|
667
|
+
this.client?.close();
|
|
668
|
+
this.client = null;
|
|
669
|
+
this.subscribedThreadIds.clear();
|
|
670
|
+
this.scheduleSnapshot();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
scheduleThreadSubscriptions() {
|
|
674
|
+
if (this.stopping || this.stopped) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (this.subscriptionSyncPromise) {
|
|
678
|
+
this.subscriptionSyncQueued = true;
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
this.subscriptionSyncPromise = this.syncThreadSubscriptions()
|
|
682
|
+
.catch(() => {
|
|
683
|
+
/* notes are handled inside syncThreadSubscriptions */
|
|
684
|
+
})
|
|
685
|
+
.finally(() => {
|
|
686
|
+
this.subscriptionSyncPromise = null;
|
|
687
|
+
if (this.subscriptionSyncQueued) {
|
|
688
|
+
this.subscriptionSyncQueued = false;
|
|
689
|
+
this.scheduleThreadSubscriptions();
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
async syncThreadSubscriptions() {
|
|
694
|
+
if (!this.client) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
this.clearMatchingNote("Live subscription sync degraded:");
|
|
698
|
+
this.clearMatchingNote("Thread subscribe failed (");
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
const candidates = [...this.threads.values()]
|
|
701
|
+
.filter((thread) => (!this.stoppedAtByThreadId.has(thread.id)
|
|
702
|
+
&& (this.ongoingThreadIds.has(thread.id)
|
|
703
|
+
|| (thread.status.type === "active" && !(0, snapshot_1.isStaleActiveSubagentThread)(thread, now))
|
|
704
|
+
|| (0, snapshot_1.isOngoingThread)(thread)
|
|
705
|
+
|| isRecentThreadSubscriptionCandidate(thread, now))))
|
|
706
|
+
.sort((left, right) => {
|
|
707
|
+
const leftActive = (left.status.type === "active" && !(0, snapshot_1.isStaleActiveSubagentThread)(left, now)) || this.ongoingThreadIds.has(left.id) ? 1 : 0;
|
|
708
|
+
const rightActive = (right.status.type === "active" && !(0, snapshot_1.isStaleActiveSubagentThread)(right, now)) || this.ongoingThreadIds.has(right.id) ? 1 : 0;
|
|
709
|
+
return rightActive - leftActive || right.updatedAt - left.updatedAt;
|
|
710
|
+
})
|
|
711
|
+
.slice(0, MAX_SUBSCRIBED_THREADS);
|
|
712
|
+
const targetIds = new Set(candidates.map((thread) => thread.id));
|
|
713
|
+
const loadedThreadIds = new Set(await withTimeout(this.client.listLoadedThreads().catch(() => []), APP_SERVER_SUBSCRIPTION_TIMEOUT_MS, "thread/loaded/list").catch((error) => {
|
|
714
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
715
|
+
this.addNote(`Live subscription sync degraded: ${message}`);
|
|
716
|
+
return [];
|
|
717
|
+
}));
|
|
718
|
+
await Promise.all([...targetIds]
|
|
719
|
+
.filter((candidateId) => !this.subscribedThreadIds.has(candidateId))
|
|
720
|
+
.map(async (threadId) => {
|
|
721
|
+
try {
|
|
722
|
+
await withTimeout(this.client?.resumeThread(threadId) ?? Promise.reject(new Error("client unavailable")), APP_SERVER_SUBSCRIPTION_TIMEOUT_MS, `thread/resume ${threadId.slice(0, 8)}`);
|
|
723
|
+
this.subscribedThreadIds.add(threadId);
|
|
724
|
+
loadedThreadIds.add(threadId);
|
|
725
|
+
await this.refreshThread(threadId);
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
729
|
+
this.addNote(`Thread subscribe failed (${threadId.slice(0, 8)}): ${message}`);
|
|
730
|
+
}
|
|
731
|
+
}));
|
|
732
|
+
await Promise.all([...this.subscribedThreadIds]
|
|
733
|
+
.filter((candidateId) => !targetIds.has(candidateId))
|
|
734
|
+
.map(async (threadId) => {
|
|
735
|
+
try {
|
|
736
|
+
await withTimeout(this.client?.unsubscribeThread(threadId) ?? Promise.reject(new Error("client unavailable")), APP_SERVER_SUBSCRIPTION_TIMEOUT_MS, `thread/unsubscribe ${threadId.slice(0, 8)}`);
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
/* ignore unsubscribe failures */
|
|
740
|
+
}
|
|
741
|
+
this.subscribedThreadIds.delete(threadId);
|
|
742
|
+
}));
|
|
743
|
+
for (const threadId of [...this.subscribedThreadIds]) {
|
|
744
|
+
if (!loadedThreadIds.has(threadId)) {
|
|
745
|
+
this.scheduleThreadRefresh(threadId);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
this.scheduleSnapshot();
|
|
749
|
+
}
|
|
750
|
+
scheduleThreadRefresh(threadId) {
|
|
751
|
+
const existing = this.threadReadTimers.get(threadId);
|
|
752
|
+
if (existing) {
|
|
753
|
+
clearTimeout(existing);
|
|
754
|
+
}
|
|
755
|
+
const timer = setTimeout(() => {
|
|
756
|
+
this.threadReadTimers.delete(threadId);
|
|
757
|
+
void this.refreshThread(threadId);
|
|
758
|
+
}, THREAD_READ_DEBOUNCE_MS);
|
|
759
|
+
timer.unref?.();
|
|
760
|
+
this.threadReadTimers.set(threadId, timer);
|
|
761
|
+
}
|
|
762
|
+
ensureThreadWatcher(threadId, path) {
|
|
763
|
+
if (!path) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const currentPath = this.threadPaths.get(threadId);
|
|
767
|
+
if (currentPath === path) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (currentPath) {
|
|
771
|
+
this.threadWatchers.get(threadId)?.close();
|
|
772
|
+
(0, node_fs_1.unwatchFile)(currentPath);
|
|
773
|
+
}
|
|
774
|
+
this.threadPaths.set(threadId, path);
|
|
775
|
+
try {
|
|
776
|
+
const watcher = (0, node_fs_1.watch)(path, { persistent: false }, () => {
|
|
777
|
+
this.scheduleThreadRefresh(threadId);
|
|
778
|
+
});
|
|
779
|
+
this.threadWatchers.set(threadId, watcher);
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
/* ignore and rely on watchFile below */
|
|
783
|
+
}
|
|
784
|
+
(0, node_fs_1.watchFile)(path, { interval: FILE_WATCH_INTERVAL_MS, persistent: false }, () => {
|
|
785
|
+
this.scheduleThreadRefresh(threadId);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
removeThread(threadId) {
|
|
789
|
+
this.threads.delete(threadId);
|
|
790
|
+
const watcher = this.threadWatchers.get(threadId);
|
|
791
|
+
watcher?.close();
|
|
792
|
+
this.threadWatchers.delete(threadId);
|
|
793
|
+
const timer = this.threadReadTimers.get(threadId);
|
|
794
|
+
if (timer) {
|
|
795
|
+
clearTimeout(timer);
|
|
796
|
+
this.threadReadTimers.delete(threadId);
|
|
797
|
+
}
|
|
798
|
+
const path = this.threadPaths.get(threadId);
|
|
799
|
+
if (path) {
|
|
800
|
+
(0, node_fs_1.unwatchFile)(path);
|
|
801
|
+
this.threadPaths.delete(threadId);
|
|
802
|
+
}
|
|
803
|
+
for (const [requestId, request] of this.pendingUserRequests.entries()) {
|
|
804
|
+
if (request.threadId === threadId) {
|
|
805
|
+
this.pendingUserRequests.delete(requestId);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const removalTimer = this.threadRemovalTimers.get(threadId);
|
|
809
|
+
if (removalTimer) {
|
|
810
|
+
clearTimeout(removalTimer);
|
|
811
|
+
this.threadRemovalTimers.delete(threadId);
|
|
812
|
+
}
|
|
813
|
+
this.clearPendingNotLoadedStop(threadId);
|
|
814
|
+
this.ongoingThreadIds.delete(threadId);
|
|
815
|
+
this.stoppedAtByThreadId.delete(threadId);
|
|
816
|
+
this.hydratedThreadIds.delete(threadId);
|
|
817
|
+
this.threadGoals.delete(threadId);
|
|
818
|
+
this.subscribedThreadIds.delete(threadId);
|
|
819
|
+
}
|
|
820
|
+
belongsToProject(message) {
|
|
821
|
+
const threadId = (0, events_1.extractThreadId)(message.params);
|
|
822
|
+
if (threadId && this.threads.has(threadId)) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
const projectPaths = [...(0, events_1.collectPaths)(message.params)];
|
|
826
|
+
return projectPaths.some((path) => path === this.projectRoot || path.startsWith(`${this.projectRoot}/`));
|
|
827
|
+
}
|
|
828
|
+
pushRecentEvent(event) {
|
|
829
|
+
const now = Date.now();
|
|
830
|
+
const createdAtMs = Date.parse(event.createdAt);
|
|
831
|
+
if (Number.isFinite(createdAtMs) && createdAtMs < now - RECENT_EVENT_RETENTION_MS) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
this.recentEvents.unshift(event);
|
|
835
|
+
const seen = new Set();
|
|
836
|
+
this.recentEvents = this.recentEvents.filter((entry) => {
|
|
837
|
+
const entryCreatedAtMs = Date.parse(entry.createdAt);
|
|
838
|
+
if (Number.isFinite(entryCreatedAtMs) && entryCreatedAtMs < now - RECENT_EVENT_RETENTION_MS) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
if (seen.has(entry.id)) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
seen.add(entry.id);
|
|
845
|
+
return true;
|
|
846
|
+
}).slice(0, MAX_RECENT_EVENTS);
|
|
847
|
+
}
|
|
848
|
+
handleAppServerNotification(notification) {
|
|
849
|
+
const diagnosticNote = (0, events_1.buildAppServerDiagnosticNote)(notification);
|
|
850
|
+
if (!this.belongsToProject(notification)) {
|
|
851
|
+
if (!(0, events_1.extractThreadId)(notification.params) && diagnosticNote) {
|
|
852
|
+
this.clearMatchingNote((0, events_1.appServerDiagnosticNotePrefix)(notification.method));
|
|
853
|
+
this.addNote(diagnosticNote);
|
|
854
|
+
this.scheduleSnapshot();
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (diagnosticNote) {
|
|
859
|
+
this.clearMatchingNote((0, events_1.appServerDiagnosticNotePrefix)(notification.method));
|
|
860
|
+
this.addNote(diagnosticNote);
|
|
861
|
+
}
|
|
862
|
+
const threadId = (0, events_1.extractThreadId)(notification.params);
|
|
863
|
+
if (threadId) {
|
|
864
|
+
let knownThread = this.threads.get(threadId) ?? null;
|
|
865
|
+
const wasOngoing = this.ongoingThreadIds.has(threadId)
|
|
866
|
+
|| (knownThread ? (0, snapshot_1.isOngoingThread)(knownThread) : false);
|
|
867
|
+
const status = notification.method === "thread/status/changed"
|
|
868
|
+
? (0, events_1.asRecord)((0, events_1.asRecord)(notification.params)?.status)
|
|
869
|
+
: null;
|
|
870
|
+
const statusType = (0, events_1.asString)(status?.type);
|
|
871
|
+
const hasFinalAgentMessage = (0, events_1.isFinalAgentMessageNotification)(notification);
|
|
872
|
+
const turn = notification.method === "turn/started" || notification.method === "turn/completed"
|
|
873
|
+
? notificationTurn(notification.params)
|
|
874
|
+
: null;
|
|
875
|
+
if (knownThread && turn) {
|
|
876
|
+
knownThread = upsertThreadTurn(knownThread, turn);
|
|
877
|
+
this.threads.set(threadId, knownThread);
|
|
878
|
+
}
|
|
879
|
+
this.applyGoalNotification(threadId, notification);
|
|
880
|
+
if (knownThread && notification.method === "thread/status/changed" && statusType) {
|
|
881
|
+
const nextStatus = statusType === "active"
|
|
882
|
+
? {
|
|
883
|
+
type: "active",
|
|
884
|
+
activeFlags: Array.isArray(status?.activeFlags)
|
|
885
|
+
? status.activeFlags.filter((flag) => typeof flag === "string")
|
|
886
|
+
: knownThread.status.type === "active" ? knownThread.status.activeFlags : undefined
|
|
887
|
+
}
|
|
888
|
+
: statusType === "notLoaded" ? { type: "notLoaded" }
|
|
889
|
+
: statusType === "idle" ? { type: "idle" }
|
|
890
|
+
: statusType === "systemError" ? { type: "systemError" }
|
|
891
|
+
: null;
|
|
892
|
+
if (nextStatus) {
|
|
893
|
+
this.threads.set(threadId, {
|
|
894
|
+
...knownThread,
|
|
895
|
+
status: nextStatus
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if ((0, events_1.shouldMarkThreadLiveFromAppServerNotification)(notification.method, statusType)) {
|
|
900
|
+
this.markThreadLive(threadId);
|
|
901
|
+
}
|
|
902
|
+
else if (hasFinalAgentMessage) {
|
|
903
|
+
this.markThreadStopped(threadId);
|
|
904
|
+
}
|
|
905
|
+
else if ((0, events_1.shouldMarkThreadStoppedFromAppServerNotification)(notification.method, statusType)) {
|
|
906
|
+
this.markThreadStopped(threadId);
|
|
907
|
+
}
|
|
908
|
+
else if ((0, events_1.shouldStopDormantThreadAfterAppServerNotification)({
|
|
909
|
+
method: notification.method,
|
|
910
|
+
statusType,
|
|
911
|
+
wasOngoing
|
|
912
|
+
})) {
|
|
913
|
+
if (notification.method === "thread/status/changed" && statusType === "notLoaded") {
|
|
914
|
+
this.schedulePendingNotLoadedStop(threadId);
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
this.markThreadStopped(threadId);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (notification.method === "thread/closed"
|
|
921
|
+
|| (notification.method === "thread/status/changed" && statusType === "notLoaded")) {
|
|
922
|
+
this.scheduleThreadSubscriptions();
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const collabReceiverThreadIds = (0, events_1.extractCollabReceiverThreadIds)(notification);
|
|
926
|
+
if (collabReceiverThreadIds.length > 0) {
|
|
927
|
+
for (const receiverThreadId of collabReceiverThreadIds) {
|
|
928
|
+
this.markThreadLive(receiverThreadId);
|
|
929
|
+
void this.refreshCollabReceiverThread(receiverThreadId, threadId);
|
|
930
|
+
}
|
|
931
|
+
this.scheduleThreadSubscriptions();
|
|
932
|
+
}
|
|
933
|
+
if (notification.method === "serverRequest/resolved") {
|
|
934
|
+
const params = (0, events_1.asRecord)(notification.params) ?? {};
|
|
935
|
+
const requestId = (0, events_1.asString)(params.requestId);
|
|
936
|
+
const pendingRequest = requestId ? this.pendingUserRequests.get(requestId) ?? null : null;
|
|
937
|
+
if (requestId) {
|
|
938
|
+
this.pendingUserRequests.delete(requestId);
|
|
939
|
+
}
|
|
940
|
+
const event = (0, events_1.buildDashboardEventFromAppServerMessage)({ projectRoot: this.projectRoot, pendingRequest }, notification);
|
|
941
|
+
if (event) {
|
|
942
|
+
this.pushRecentEvent(event);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
const event = (0, events_1.buildDashboardEventFromAppServerMessage)({ projectRoot: this.projectRoot }, notification);
|
|
947
|
+
if (event) {
|
|
948
|
+
if (event.kind === "subagent" && event.threadId) {
|
|
949
|
+
this.markThreadLive(event.threadId);
|
|
950
|
+
}
|
|
951
|
+
this.pushRecentEvent(event);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (threadId) {
|
|
955
|
+
this.scheduleThreadRefresh(threadId);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
void this.discoverThreads();
|
|
959
|
+
}
|
|
960
|
+
this.scheduleSnapshot();
|
|
961
|
+
}
|
|
962
|
+
handleAppServerServerRequest(request) {
|
|
963
|
+
if (!this.belongsToProject(request)) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (request.method === "currentTime/read") {
|
|
967
|
+
this.client?.respondToServerRequest(request.id, {
|
|
968
|
+
currentTimeAt: Math.floor(Date.now() / 1000)
|
|
969
|
+
});
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const needsUser = (0, events_1.buildNeedsUserStateFromServerRequest)(request);
|
|
973
|
+
if (needsUser) {
|
|
974
|
+
this.pendingUserRequests.set(needsUser.requestId, needsUser);
|
|
975
|
+
}
|
|
976
|
+
const event = (0, events_1.buildDashboardEventFromAppServerMessage)({ projectRoot: this.projectRoot, pendingRequest: needsUser }, request);
|
|
977
|
+
if (event) {
|
|
978
|
+
this.pushRecentEvent(event);
|
|
979
|
+
}
|
|
980
|
+
if (request.method === "item/tool/call") {
|
|
981
|
+
const numericRequestId = request.id;
|
|
982
|
+
const requestParams = (0, events_1.asRecord)(request.params);
|
|
983
|
+
const tool = (0, events_1.asString)(requestParams?.tool);
|
|
984
|
+
const namespace = (0, events_1.asString)(requestParams?.namespace);
|
|
985
|
+
this.client?.respondToDynamicToolCallUnsupported(numericRequestId, namespace && tool ? `${namespace}.${tool}` : tool);
|
|
986
|
+
}
|
|
987
|
+
const threadId = (0, events_1.extractThreadId)(request.params);
|
|
988
|
+
if (threadId) {
|
|
989
|
+
this.markThreadLive(threadId);
|
|
990
|
+
this.scheduleThreadRefresh(threadId);
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
void this.discoverThreads();
|
|
994
|
+
}
|
|
995
|
+
this.scheduleSnapshot();
|
|
996
|
+
}
|
|
997
|
+
applyGoalNotification(threadId, notification) {
|
|
998
|
+
if (notification.method === "thread/goal/cleared") {
|
|
999
|
+
this.threadGoals.delete(threadId);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (notification.method !== "thread/goal/updated") {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const goal = (0, goal_1.codexGoalToAgentGoal)((0, events_1.asRecord)(notification.params)?.goal);
|
|
1006
|
+
if (goal) {
|
|
1007
|
+
this.threadGoals.set(threadId, goal);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async refreshThreadGoal(threadId) {
|
|
1011
|
+
if (!this.client) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
const goal = (0, goal_1.codexGoalToAgentGoal)(await withTimeout(this.client.getThreadGoal(threadId), this.appServerRequestTimeoutMs, `thread/goal/get ${threadId.slice(0, 8)}`));
|
|
1016
|
+
if (goal) {
|
|
1017
|
+
this.threadGoals.set(threadId, goal);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
this.threadGoals.delete(threadId);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
this.recoverTimedOutAppServerClient(error);
|
|
1025
|
+
// Goal metadata is best-effort and should not disturb thread visibility.
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async refreshThread(threadId, listedThread = null) {
|
|
1029
|
+
if (!this.client) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const refreshGeneration = (this.threadRefreshGenerations.get(threadId) ?? 0) + 1;
|
|
1033
|
+
this.threadRefreshGenerations.set(threadId, refreshGeneration);
|
|
1034
|
+
try {
|
|
1035
|
+
const wasHydrated = this.hydratedThreadIds.has(threadId);
|
|
1036
|
+
const hasLiveSubscription = this.subscribedThreadIds.has(threadId);
|
|
1037
|
+
const previousThread = this.threads.get(threadId) ?? null;
|
|
1038
|
+
const wasOngoing = this.ongoingThreadIds.has(threadId)
|
|
1039
|
+
|| (previousThread ? (0, snapshot_1.isOngoingThread)(previousThread) : false);
|
|
1040
|
+
let readThread;
|
|
1041
|
+
try {
|
|
1042
|
+
readThread = await (0, codex_thread_query_1.readCodexThreadWithTimeout)(this.client, threadId, this.appServerRequestTimeoutMs);
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
this.recoverTimedOutAppServerClient(error);
|
|
1046
|
+
if (!listedThread) {
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
readThread = listedThread;
|
|
1050
|
+
}
|
|
1051
|
+
if (this.threadRefreshGenerations.get(threadId) !== refreshGeneration) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
// A file-watch reread may arrive immediately after thread/list hydration.
|
|
1055
|
+
// Preserve the freshest known list metadata instead of letting an older
|
|
1056
|
+
// thread/read payload move the workload backwards in time.
|
|
1057
|
+
const freshestKnownThread = listedThread ?? previousThread;
|
|
1058
|
+
const thread = freshestKnownThread
|
|
1059
|
+
? {
|
|
1060
|
+
...readThread,
|
|
1061
|
+
status: listedThread ? listedThread.status : readThread.status,
|
|
1062
|
+
updatedAt: Math.max(readThread.updatedAt, freshestKnownThread.updatedAt),
|
|
1063
|
+
path: freshestKnownThread.path ?? readThread.path
|
|
1064
|
+
}
|
|
1065
|
+
: readThread;
|
|
1066
|
+
this.clearMatchingNote(`Thread refresh failed (${threadId.slice(0, 8)}):`);
|
|
1067
|
+
this.threads.set(threadId, thread);
|
|
1068
|
+
await this.refreshThreadGoal(threadId);
|
|
1069
|
+
const settledDormantSubagent = isSettledDormantSubagentThread(thread);
|
|
1070
|
+
const awaitingFinalAnswer = !settledDormantSubagent && threadStillAwaitsFinalAnswer(thread);
|
|
1071
|
+
const shouldPromoteAwaitingFinalAnswer = awaitingFinalAnswer
|
|
1072
|
+
&& (hasLiveSubscription
|
|
1073
|
+
|| wasOngoing
|
|
1074
|
+
|| isFreshEnoughToPromoteAwaitingFinalAnswer(thread));
|
|
1075
|
+
if ((0, snapshot_1.isOngoingThread)(thread) || shouldPromoteAwaitingFinalAnswer) {
|
|
1076
|
+
this.markThreadLive(threadId);
|
|
1077
|
+
}
|
|
1078
|
+
else if (wasOngoing) {
|
|
1079
|
+
const nextMessage = (0, events_1.latestThreadAgentMessage)(thread);
|
|
1080
|
+
const hasFinalAnswer = nextMessage?.phase === "final_answer";
|
|
1081
|
+
const pendingNotLoadedStop = thread.status.type === "notLoaded" && this.pendingNotLoadedStopTimers.has(threadId);
|
|
1082
|
+
if (pendingNotLoadedStop) {
|
|
1083
|
+
// Wait for the scheduled reread confirmation before releasing a desk.
|
|
1084
|
+
}
|
|
1085
|
+
else if ((0, snapshot_1.isStaleActiveSubagentThread)(thread)) {
|
|
1086
|
+
this.markThreadStopped(threadId);
|
|
1087
|
+
}
|
|
1088
|
+
else if (settledDormantSubagent) {
|
|
1089
|
+
this.markThreadStopped(threadId);
|
|
1090
|
+
}
|
|
1091
|
+
else if (thread.status.type === "systemError" || hasFinalAnswer) {
|
|
1092
|
+
this.markThreadStopped(threadId);
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
this.markThreadLive(threadId);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const previousMessage = previousThread ? (0, events_1.latestThreadAgentMessage)(previousThread) : null;
|
|
1099
|
+
const nextMessage = (0, events_1.latestThreadAgentMessage)(thread);
|
|
1100
|
+
if (wasHydrated
|
|
1101
|
+
&& nextMessage
|
|
1102
|
+
&& (0, events_1.latestThreadAgentMessageIsInLastTurn)(thread)
|
|
1103
|
+
&& (nextMessage.itemId !== previousMessage?.itemId
|
|
1104
|
+
|| nextMessage.text !== previousMessage?.text)) {
|
|
1105
|
+
// Even subscribed desktop sessions can miss a terminal message notification;
|
|
1106
|
+
// rereads backfill the toast event while equivalent live events still dedupe.
|
|
1107
|
+
const messageEvent = (0, events_1.buildThreadReadAgentMessageEvent)({ projectRoot: this.projectRoot }, thread);
|
|
1108
|
+
if (messageEvent && !(0, events_1.hasEquivalentRecentMessageEvent)(this.recentEvents, messageEvent)) {
|
|
1109
|
+
this.pushRecentEvent(messageEvent);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
this.ensureThreadWatcher(threadId, thread.path);
|
|
1113
|
+
const rolloutEvents = await (0, rollout_hooks_1.readRecentRolloutHookEvents)(this.projectRoot, threadId, thread.path, thread.updatedAt);
|
|
1114
|
+
for (const event of rolloutEvents) {
|
|
1115
|
+
this.pushRecentEvent(event);
|
|
1116
|
+
}
|
|
1117
|
+
this.hydratedThreadIds.add(threadId);
|
|
1118
|
+
this.scheduleSnapshot();
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1122
|
+
this.addNote(`Thread refresh failed (${threadId.slice(0, 8)}): ${message}`);
|
|
1123
|
+
this.scheduleSnapshot();
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
async refreshCollabReceiverThread(threadId, senderThreadId) {
|
|
1127
|
+
if (!this.client) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
try {
|
|
1131
|
+
const thread = await this.client.readThread(threadId);
|
|
1132
|
+
const parentThreadId = (0, snapshot_1.parentThreadIdForThread)(thread);
|
|
1133
|
+
const canonicalCwd = (0, project_paths_1.canonicalizeProjectPath)(thread.cwd) ?? thread.cwd;
|
|
1134
|
+
const belongsToProject = canonicalCwd === this.projectRoot
|
|
1135
|
+
|| canonicalCwd.startsWith(`${this.projectRoot}/`)
|
|
1136
|
+
|| Boolean(parentThreadId && this.threads.has(parentThreadId))
|
|
1137
|
+
|| Boolean(parentThreadId && senderThreadId === parentThreadId);
|
|
1138
|
+
if (!belongsToProject) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
this.threads.set(threadId, thread);
|
|
1142
|
+
await this.refreshThreadGoal(threadId);
|
|
1143
|
+
if ((0, snapshot_1.isOngoingThread)(thread)) {
|
|
1144
|
+
this.markThreadLive(threadId);
|
|
1145
|
+
}
|
|
1146
|
+
this.ensureThreadWatcher(threadId, thread.path);
|
|
1147
|
+
this.scheduleThreadSubscriptions();
|
|
1148
|
+
this.scheduleSnapshot();
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
this.scheduleThreadRefresh(threadId);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
watchRoomsFile() {
|
|
1155
|
+
try {
|
|
1156
|
+
this.roomWatcher = (0, node_fs_1.watch)(this.roomConfigPath, { persistent: false }, () => {
|
|
1157
|
+
this.scheduleSnapshot();
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
catch {
|
|
1161
|
+
this.roomWatcher = null;
|
|
1162
|
+
}
|
|
1163
|
+
(0, node_fs_1.watchFile)(this.roomConfigPath, { interval: FILE_WATCH_INTERVAL_MS, persistent: false }, () => {
|
|
1164
|
+
this.scheduleSnapshot();
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
async refreshCloudTasks() {
|
|
1168
|
+
if (!this.includeCloud) {
|
|
1169
|
+
this.cloudTasks = [];
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
try {
|
|
1173
|
+
const listed = await (0, cloud_1.listCloudTasks)(10);
|
|
1174
|
+
this.cloudTasks = (0, snapshot_1.filterProjectCloudTasks)(listed, this.projectRoot);
|
|
1175
|
+
this.setCloudErrorNote(null);
|
|
1176
|
+
this.scheduleSnapshot();
|
|
1177
|
+
}
|
|
1178
|
+
catch (error) {
|
|
1179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1180
|
+
this.setCloudErrorNote(message);
|
|
1181
|
+
this.scheduleSnapshot();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
scheduleSnapshot() {
|
|
1185
|
+
if (this.stopping || this.stopped) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (this.snapshotTimer) {
|
|
1189
|
+
clearTimeout(this.snapshotTimer);
|
|
1190
|
+
}
|
|
1191
|
+
this.snapshotTimer = setTimeout(() => {
|
|
1192
|
+
void this.rebuildSnapshot();
|
|
1193
|
+
}, SNAPSHOT_DEBOUNCE_MS);
|
|
1194
|
+
this.snapshotTimer.unref?.();
|
|
1195
|
+
}
|
|
1196
|
+
async rebuildSnapshot() {
|
|
1197
|
+
if (this.stopping || this.stopped) {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
this.snapshotTimer = null;
|
|
1201
|
+
this.snapshotRebuildRequested += 1;
|
|
1202
|
+
if (!this.snapshotRebuildPromise) {
|
|
1203
|
+
const pump = this.runSnapshotRebuildPump();
|
|
1204
|
+
this.snapshotRebuildPromise = pump.finally(() => {
|
|
1205
|
+
this.snapshotRebuildPromise = null;
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
await this.snapshotRebuildPromise;
|
|
1209
|
+
}
|
|
1210
|
+
async runSnapshotRebuildPump() {
|
|
1211
|
+
while (this.snapshotRebuildCompleted < this.snapshotRebuildRequested) {
|
|
1212
|
+
if (this.stopping || this.stopped) {
|
|
1213
|
+
this.snapshotRebuildCompleted = this.snapshotRebuildRequested;
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const targetGeneration = this.snapshotRebuildRequested;
|
|
1217
|
+
try {
|
|
1218
|
+
await this.assembleLatestSnapshot(targetGeneration);
|
|
1219
|
+
}
|
|
1220
|
+
catch (error) {
|
|
1221
|
+
if (this.stopping || this.stopped) {
|
|
1222
|
+
this.snapshotRebuildCompleted = this.snapshotRebuildRequested;
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
throw error;
|
|
1226
|
+
}
|
|
1227
|
+
this.snapshotRebuildCompleted = targetGeneration;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async assembleLatestSnapshot(targetGeneration) {
|
|
1231
|
+
const needsUserByThreadId = new Map();
|
|
1232
|
+
for (const pending of this.pendingUserRequests.values()) {
|
|
1233
|
+
needsUserByThreadId.set(pending.threadId, pending);
|
|
1234
|
+
}
|
|
1235
|
+
const coordinator = await this.ensureSnapshotCoordinator();
|
|
1236
|
+
const snapshot = await coordinator.buildSnapshot({
|
|
1237
|
+
threads: Array.from(this.threads.values()),
|
|
1238
|
+
cloudTasks: this.cloudTasks,
|
|
1239
|
+
events: this.recentEvents,
|
|
1240
|
+
notes: Array.from(this.notes),
|
|
1241
|
+
needsUserByThreadId,
|
|
1242
|
+
goalsByThreadId: this.threadGoals,
|
|
1243
|
+
subscribedThreadIds: this.subscribedThreadIds,
|
|
1244
|
+
stoppedAtByThreadId: this.stoppedAtByThreadId,
|
|
1245
|
+
ongoingThreadIds: this.ongoingThreadIds
|
|
1246
|
+
});
|
|
1247
|
+
if (snapshot && targetGeneration === this.snapshotRebuildRequested) {
|
|
1248
|
+
this.snapshot = snapshot;
|
|
1249
|
+
this.emit("snapshot", snapshot);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
async ensureSnapshotCoordinator() {
|
|
1253
|
+
if (this.stopping || this.stopped) {
|
|
1254
|
+
throw new Error("ProjectLiveMonitor has been stopped.");
|
|
1255
|
+
}
|
|
1256
|
+
if (this.snapshotCoordinator) {
|
|
1257
|
+
return this.snapshotCoordinator;
|
|
1258
|
+
}
|
|
1259
|
+
if (!this.snapshotCoordinatorPromise) {
|
|
1260
|
+
this.snapshotCoordinatorPromise = Promise.resolve().then(() => __importStar(require("./snapshot"))).then(async ({ createProjectSnapshotCoordinator }) => {
|
|
1261
|
+
const coordinator = await createProjectSnapshotCoordinator({
|
|
1262
|
+
projectRoot: this.projectRoot,
|
|
1263
|
+
localLimit: this.localLimit,
|
|
1264
|
+
includeManagedCloud: false
|
|
1265
|
+
});
|
|
1266
|
+
this.snapshotCoordinator = coordinator;
|
|
1267
|
+
return coordinator;
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
return this.snapshotCoordinatorPromise;
|
|
1271
|
+
}
|
|
1272
|
+
async refreshSecondarySources(reason = "interval", minimumAgeMs = DISCOVERY_INTERVAL_MS) {
|
|
1273
|
+
if (this.stopping || this.stopped) {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
const coordinator = await this.ensureSnapshotCoordinator();
|
|
1278
|
+
if (this.stopping || this.stopped) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (await coordinator.refreshIfStale(reason, minimumAgeMs)) {
|
|
1282
|
+
this.scheduleSnapshot();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
catch (error) {
|
|
1286
|
+
if (!this.stopping && !this.stopped) {
|
|
1287
|
+
throw error;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
markThreadLive(threadId) {
|
|
1292
|
+
this.clearPendingNotLoadedStop(threadId);
|
|
1293
|
+
this.ongoingThreadIds.add(threadId);
|
|
1294
|
+
this.stoppedAtByThreadId.delete(threadId);
|
|
1295
|
+
const removalTimer = this.threadRemovalTimers.get(threadId);
|
|
1296
|
+
if (removalTimer) {
|
|
1297
|
+
clearTimeout(removalTimer);
|
|
1298
|
+
this.threadRemovalTimers.delete(threadId);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
markThreadStopped(threadId) {
|
|
1302
|
+
if (!this.threads.has(threadId)) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (this.stoppedAtByThreadId.has(threadId)) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
this.clearPendingNotLoadedStop(threadId);
|
|
1309
|
+
this.ongoingThreadIds.delete(threadId);
|
|
1310
|
+
this.stoppedAtByThreadId.set(threadId, Date.now());
|
|
1311
|
+
const removalTimer = this.threadRemovalTimers.get(threadId);
|
|
1312
|
+
if (removalTimer) {
|
|
1313
|
+
clearTimeout(removalTimer);
|
|
1314
|
+
}
|
|
1315
|
+
const timer = setTimeout(() => {
|
|
1316
|
+
this.threadRemovalTimers.delete(threadId);
|
|
1317
|
+
this.removeThread(threadId);
|
|
1318
|
+
this.scheduleSnapshot();
|
|
1319
|
+
}, workload_1.RECENT_DONE_GRACE_MS + STOPPED_THREAD_REMOVAL_BUFFER_MS);
|
|
1320
|
+
timer.unref?.();
|
|
1321
|
+
this.threadRemovalTimers.set(threadId, timer);
|
|
1322
|
+
}
|
|
1323
|
+
clearPendingNotLoadedStop(threadId) {
|
|
1324
|
+
const timer = this.pendingNotLoadedStopTimers.get(threadId);
|
|
1325
|
+
if (!timer) {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
clearTimeout(timer);
|
|
1329
|
+
this.pendingNotLoadedStopTimers.delete(threadId);
|
|
1330
|
+
}
|
|
1331
|
+
schedulePendingNotLoadedStop(threadId) {
|
|
1332
|
+
if (!this.threads.has(threadId)) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
this.clearPendingNotLoadedStop(threadId);
|
|
1336
|
+
const timer = setTimeout(() => {
|
|
1337
|
+
this.pendingNotLoadedStopTimers.delete(threadId);
|
|
1338
|
+
void this.confirmDormantNotLoadedThread(threadId);
|
|
1339
|
+
}, NOT_LOADED_STOP_DEBOUNCE_MS);
|
|
1340
|
+
timer.unref?.();
|
|
1341
|
+
this.pendingNotLoadedStopTimers.set(threadId, timer);
|
|
1342
|
+
}
|
|
1343
|
+
async confirmDormantNotLoadedThread(threadId) {
|
|
1344
|
+
if (!this.threads.has(threadId)) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
await this.refreshThread(threadId);
|
|
1348
|
+
const thread = this.threads.get(threadId) ?? null;
|
|
1349
|
+
if (!thread) {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if ((0, snapshot_1.isOngoingThread)(thread)) {
|
|
1353
|
+
this.markThreadLive(threadId);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (thread.status.type !== "notLoaded") {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const latestMessage = (0, events_1.latestThreadAgentMessage)(thread);
|
|
1360
|
+
if (isSettledDormantSubagentThread(thread) || latestMessage?.phase === "final_answer") {
|
|
1361
|
+
this.markThreadStopped(threadId);
|
|
1362
|
+
}
|
|
1363
|
+
else {
|
|
1364
|
+
this.markThreadLive(threadId);
|
|
1365
|
+
}
|
|
1366
|
+
this.scheduleSnapshot();
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
exports.ProjectLiveMonitor = ProjectLiveMonitor;
|
|
1370
|
+
function normalizeUserInputAnswers(questions, rawAnswers) {
|
|
1371
|
+
if (!rawAnswers || typeof rawAnswers !== "object" || Array.isArray(rawAnswers)) {
|
|
1372
|
+
throw new Error("Input answers must be an object keyed by question id.");
|
|
1373
|
+
}
|
|
1374
|
+
const questionsById = new Map((questions ?? []).map((question) => [question.id, question]));
|
|
1375
|
+
const requiredQuestions = [...questionsById.values()].filter((question) => question.required !== false);
|
|
1376
|
+
const answerEntries = Object.entries(rawAnswers);
|
|
1377
|
+
if (requiredQuestions.length > 0 && answerEntries.length === 0) {
|
|
1378
|
+
throw new Error("Answer at least one required question before sending input.");
|
|
1379
|
+
}
|
|
1380
|
+
const normalized = {};
|
|
1381
|
+
for (const [questionId, answer] of answerEntries) {
|
|
1382
|
+
const question = questionsById.get(questionId) ?? null;
|
|
1383
|
+
const answerRecord = typeof answer === "object" && answer && !Array.isArray(answer)
|
|
1384
|
+
? answer
|
|
1385
|
+
: null;
|
|
1386
|
+
const values = Array.isArray(answerRecord?.answers)
|
|
1387
|
+
? answerRecord.answers
|
|
1388
|
+
.map((value) => typeof value === "string" ? value.trim() : "")
|
|
1389
|
+
.filter((value) => value.length > 0)
|
|
1390
|
+
: [];
|
|
1391
|
+
if (values.length === 0) {
|
|
1392
|
+
throw new Error(`Question ${questionId} requires at least one answer.`);
|
|
1393
|
+
}
|
|
1394
|
+
if (question?.options && question.isOther !== true) {
|
|
1395
|
+
const allowed = new Set(question.options.map((option) => option.label));
|
|
1396
|
+
const invalid = values.find((value) => !allowed.has(value));
|
|
1397
|
+
if (invalid) {
|
|
1398
|
+
throw new Error(`Question ${question.header} contains an unsupported answer.`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
normalized[questionId] = { answers: values };
|
|
1402
|
+
}
|
|
1403
|
+
if (questionsById.size > 0) {
|
|
1404
|
+
for (const question of questionsById.values()) {
|
|
1405
|
+
if (question.required === false) {
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if (!normalized[question.id]) {
|
|
1409
|
+
throw new Error(`Question ${question.header} is still unanswered.`);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return normalized;
|
|
1414
|
+
}
|
|
1415
|
+
function legacyReviewDecision(decision) {
|
|
1416
|
+
switch (decision) {
|
|
1417
|
+
case "accept":
|
|
1418
|
+
return "approved";
|
|
1419
|
+
case "acceptForSession":
|
|
1420
|
+
return "approved_for_session";
|
|
1421
|
+
case "decline":
|
|
1422
|
+
return "denied";
|
|
1423
|
+
case "cancel":
|
|
1424
|
+
return "abort";
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function normalizeMcpElicitationContent(requestedSchema, answers) {
|
|
1428
|
+
const content = {};
|
|
1429
|
+
const properties = requestedSchema && typeof requestedSchema.properties === "object" && requestedSchema.properties && !Array.isArray(requestedSchema.properties)
|
|
1430
|
+
? requestedSchema.properties
|
|
1431
|
+
: {};
|
|
1432
|
+
for (const [questionId, answer] of Object.entries(answers)) {
|
|
1433
|
+
const values = answer.answers;
|
|
1434
|
+
const firstValue = values[0] ?? "";
|
|
1435
|
+
const propertySchema = typeof properties[questionId] === "object" && properties[questionId] && !Array.isArray(properties[questionId])
|
|
1436
|
+
? properties[questionId]
|
|
1437
|
+
: null;
|
|
1438
|
+
if (propertySchema?.type === "number" || propertySchema?.type === "integer") {
|
|
1439
|
+
const numeric = Number(firstValue);
|
|
1440
|
+
if (!Number.isFinite(numeric)) {
|
|
1441
|
+
throw new Error(`Question ${questionId} requires a numeric answer.`);
|
|
1442
|
+
}
|
|
1443
|
+
content[questionId] = propertySchema.type === "integer" ? Math.trunc(numeric) : numeric;
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
if (propertySchema?.type === "boolean") {
|
|
1447
|
+
if (firstValue !== "true" && firstValue !== "false") {
|
|
1448
|
+
throw new Error(`Question ${questionId} requires true or false.`);
|
|
1449
|
+
}
|
|
1450
|
+
content[questionId] = firstValue === "true";
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (propertySchema?.type === "array") {
|
|
1454
|
+
content[questionId] = values;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
content[questionId] = firstValue;
|
|
1458
|
+
}
|
|
1459
|
+
return content;
|
|
1460
|
+
}
|
|
1461
|
+
//# sourceMappingURL=live-monitor.js.map
|