querysub 0.680.0 → 0.682.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/package.json +1 -1
- package/src/0-path-value-core/PathRouter.ts +2 -2
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +18 -16
- package/src/2-proxy/PathValueProxyWatcher.ts +1 -1
- package/src/5-diagnostics/Modal.tsx +10 -2
- package/src/diagnostics/FunctionCallInfoState.ts +29 -89
- package/src/diagnostics/logs/errorTickets/TicketPage.tsx +151 -32
- package/src/diagnostics/logs/errorTickets/autoFixer.ts +303 -57
- package/src/diagnostics/logs/errorTickets/autoFixerController.ts +4 -4
- package/src/diagnostics/logs/errorTickets/ticketTypes.ts +17 -0
- package/src/diagnostics/logs/errorTickets/tickets.ts +49 -24
- package/src/diagnostics/statsDefinitions.tsx +8 -19
- package/src/library-components/URLParam.ts +11 -3
|
@@ -9,7 +9,7 @@ import { getControllerNodeId } from "../../../-g-core-values/NodeCapabilities";
|
|
|
9
9
|
import { assertIsManagementUser } from "../../managementPages";
|
|
10
10
|
import { getSyncedController } from "../../../library-components/SyncedController";
|
|
11
11
|
import { AutoFixerControllerBase } from "./autoFixerController";
|
|
12
|
-
import { Ticket, TicketComment, TicketPatchStatus, TicketState } from "./ticketTypes";
|
|
12
|
+
import { AutoFixerRunStatus, Ticket, TicketComment, TicketPatchStatus, TicketState } from "./ticketTypes";
|
|
13
13
|
|
|
14
14
|
const TICKET_CACHE_POLL_INTERVAL = timeInMinute * 5;
|
|
15
15
|
|
|
@@ -107,27 +107,30 @@ class TicketService {
|
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
// One ticket write for the whole batch, so a hot reload mid-apply can't leave half the statuses recorded.
|
|
111
|
+
public async setPatchStatuses(id: string, commentIds: string[], status: TicketPatchStatus): Promise<void> {
|
|
111
112
|
await mutateTicket(id, ticket => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
for (let commentId of commentIds) {
|
|
114
|
+
let comment = ticket.comments.find(c => c.id === commentId);
|
|
115
|
+
if (!comment) {
|
|
116
|
+
throw new Error(`Comment ${commentId} not found in ticket ${id}`);
|
|
117
|
+
}
|
|
118
|
+
if (comment.kind !== "patch") {
|
|
119
|
+
throw new Error(`Comment ${commentId} is not a patch comment`);
|
|
120
|
+
}
|
|
121
|
+
comment.patchStatus = status;
|
|
122
|
+
comment.patchStatusTime = Date.now();
|
|
118
123
|
}
|
|
119
|
-
comment.patchStatus = status;
|
|
120
|
-
comment.patchStatusTime = Date.now();
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
// Routes to a registered autofixer node, which
|
|
125
|
-
public async
|
|
127
|
+
// Routes to a registered autofixer node, which validates the whole batch, records the applied statuses (via setPatchStatuses), and writes every file — in that order, so its own hot reload happens after everything else is durable.
|
|
128
|
+
public async applyPatches(id: string, commentIds: string[]): Promise<void> {
|
|
126
129
|
let errors: string[] = [];
|
|
127
130
|
let applied = false;
|
|
128
131
|
for (let nodeId of Array.from(TicketService.autoFixerNodes)) {
|
|
129
132
|
try {
|
|
130
|
-
await AutoFixerControllerBase.nodes[nodeId].
|
|
133
|
+
await AutoFixerControllerBase.nodes[nodeId].applyPatches(id, commentIds);
|
|
131
134
|
applied = true;
|
|
132
135
|
break;
|
|
133
136
|
} catch (e) {
|
|
@@ -136,11 +139,10 @@ class TicketService {
|
|
|
136
139
|
}
|
|
137
140
|
if (!applied) {
|
|
138
141
|
if (TicketService.autoFixerNodes.size === 0) {
|
|
139
|
-
throw new Error(`No autofixer is connected, so
|
|
142
|
+
throw new Error(`No autofixer is connected, so patches cannot be applied. Run \`yarn autofix\` in the repository the patches target, or trigger an AI run from the ticket page to register this server.`);
|
|
140
143
|
}
|
|
141
|
-
throw new Error(`Failed to apply
|
|
144
|
+
throw new Error(`Failed to apply patches on all connected autofixer nodes:\n${errors.join("\n")}`);
|
|
142
145
|
}
|
|
143
|
-
await this.setPatchStatus(id, commentId, "applied");
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
private static autoFixerNodes = new Set<string>();
|
|
@@ -190,8 +192,8 @@ export const TicketServiceBase = SocketFunction.register(
|
|
|
190
192
|
addComment: {},
|
|
191
193
|
updateCommentText: {},
|
|
192
194
|
deleteComment: {},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
+
setPatchStatuses: {},
|
|
196
|
+
applyPatches: {},
|
|
195
197
|
registerAutoFixerSERVICE: {},
|
|
196
198
|
watchTicketsSERVICE: {},
|
|
197
199
|
}),
|
|
@@ -244,12 +246,32 @@ class TicketData {
|
|
|
244
246
|
await TicketServiceBase.nodes[await getTicketServiceNode()].deleteComment(id, commentId);
|
|
245
247
|
}
|
|
246
248
|
|
|
247
|
-
public async
|
|
248
|
-
await TicketServiceBase.nodes[await getTicketServiceNode()].
|
|
249
|
+
public async setPatchStatuses(id: string, commentIds: string[], status: TicketPatchStatus): Promise<void> {
|
|
250
|
+
await TicketServiceBase.nodes[await getTicketServiceNode()].setPatchStatuses(id, commentIds, status);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Patches touch source code, which lives on THIS node (the server the browser is connected to) — never on the ticket service node, which manages tickets, not source. The import is dynamic for the same reason as the AI methods below: this file is loaded in the browser, and autoFixer pulls in node-only modules.
|
|
254
|
+
public async applyPatches(id: string, commentIds: string[]): Promise<void> {
|
|
255
|
+
let { applyPatches } = await import("./autoFixer");
|
|
256
|
+
await applyPatches(id, commentIds);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The AI investigation methods deliberately run on THIS node (the server the browser is connected to), not the ticket service node — in debug mode that is the local dev server, which has the repos checked out and can spawn claude. The autofixer module is imported dynamically because it pulls in node-only modules, and this file is also loaded in the browser.
|
|
260
|
+
public async startAIInvestigation(ticketId: string, model?: string): Promise<void> {
|
|
261
|
+
let { startTicketInvestigation } = await import("./autoFixer");
|
|
262
|
+
await startTicketInvestigation(ticketId, model);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public async stopAIInvestigation(ticketId: string): Promise<void> {
|
|
266
|
+
let { requestStopTicketInvestigation } = await import("./autoFixer");
|
|
267
|
+
requestStopTicketInvestigation(ticketId);
|
|
249
268
|
}
|
|
250
269
|
|
|
251
|
-
public async
|
|
252
|
-
|
|
270
|
+
public async getAIStatus(): Promise<AutoFixerRunStatus> {
|
|
271
|
+
let { getAutoFixerRunStatus, ensureDefaultModelProbed } = await import("./autoFixer");
|
|
272
|
+
// The probe is async and slow the first time — kick it off and let a later status poll pick up the resolved default.
|
|
273
|
+
ensureDefaultModelProbed();
|
|
274
|
+
return getAutoFixerRunStatus();
|
|
253
275
|
}
|
|
254
276
|
|
|
255
277
|
// Live updates: browser → this HTTP node → ticket service node, with change notifications flowing back down the same two hops.
|
|
@@ -312,8 +334,11 @@ export const TicketDataBase = SocketFunction.register(
|
|
|
312
334
|
addComment: {},
|
|
313
335
|
updateCommentText: {},
|
|
314
336
|
deleteComment: {},
|
|
315
|
-
|
|
316
|
-
|
|
337
|
+
setPatchStatuses: {},
|
|
338
|
+
applyPatches: {},
|
|
339
|
+
startAIInvestigation: {},
|
|
340
|
+
stopAIInvestigation: {},
|
|
341
|
+
getAIStatus: {},
|
|
317
342
|
watchTicketsHTTP: {},
|
|
318
343
|
receiveTicketsChangedHTTP: {},
|
|
319
344
|
receiveTicketsChangedBrowser: {},
|
|
@@ -133,7 +133,6 @@ export function getQuerysubStatsSync(): StatDefinition[] {
|
|
|
133
133
|
let state = callState();
|
|
134
134
|
|
|
135
135
|
let perFn = state.perFunctionStats;
|
|
136
|
-
let avgFn = (s: { count: number; sum: number }) => s.count && s.sum / s.count || 0;
|
|
137
136
|
|
|
138
137
|
let callStatDefs: StatDefinition[] = [
|
|
139
138
|
makeCallStatSync({
|
|
@@ -142,38 +141,28 @@ export function getQuerysubStatsSync(): StatDefinition[] {
|
|
|
142
141
|
getFnValue: fn => fn.totalCalls,
|
|
143
142
|
}),
|
|
144
143
|
makeCallStatSync({
|
|
145
|
-
perFn, category: "Calls", title: "
|
|
144
|
+
perFn, category: "Calls", title: "Max rejected reruns (one call)", emoji: "⚠️",
|
|
146
145
|
threshold: 10,
|
|
147
|
-
getFnValue: fn => fn.
|
|
146
|
+
getFnValue: fn => fn.maxFullReruns,
|
|
148
147
|
}),
|
|
149
148
|
makeCallStatSync({
|
|
150
|
-
perFn, category: "Calls", title: "
|
|
149
|
+
perFn, category: "Calls", title: "Max reruns to sync data (one call)", emoji: "♻️",
|
|
151
150
|
threshold: 10,
|
|
152
|
-
getFnValue: fn => fn.
|
|
153
|
-
}),
|
|
154
|
-
makeCallStatSync({
|
|
155
|
-
perFn, category: "Calls", title: "Calls with multiple resyncs", emoji: "🔦",
|
|
156
|
-
threshold: 10,
|
|
157
|
-
getFnValue: fn => fn.callsWithMultipleInternalRuns,
|
|
158
|
-
}),
|
|
159
|
-
makeCallStatSync({
|
|
160
|
-
perFn, category: "Calls", title: "Calls with multiple rejections", emoji: "📈",
|
|
161
|
-
threshold: 10,
|
|
162
|
-
getFnValue: fn => fn.callsWithCascadingRuns,
|
|
151
|
+
getFnValue: fn => fn.maxInternalReruns,
|
|
163
152
|
}),
|
|
164
153
|
];
|
|
165
154
|
|
|
166
155
|
let timingStatDefs: StatDefinition[] = [
|
|
167
156
|
makeCallStatSync({
|
|
168
|
-
perFn, category: "Timing", title: "
|
|
157
|
+
perFn, category: "Timing", title: "Max evaluation time (one call)", emoji: "💫",
|
|
169
158
|
threshold: 100,
|
|
170
|
-
getFnValue: fn =>
|
|
159
|
+
getFnValue: fn => fn.maxEvalTime,
|
|
171
160
|
formatValue: formatTime,
|
|
172
161
|
}),
|
|
173
162
|
makeCallStatSync({
|
|
174
|
-
perFn, category: "Timing", title: "
|
|
163
|
+
perFn, category: "Timing", title: "Max full time (sync, etc, one call)", emoji: "🕒",
|
|
175
164
|
threshold: 1000,
|
|
176
|
-
getFnValue: fn =>
|
|
165
|
+
getFnValue: fn => fn.maxTotalTime,
|
|
177
166
|
formatValue: formatTime,
|
|
178
167
|
}),
|
|
179
168
|
];
|
|
@@ -11,6 +11,7 @@ import { logErrors } from "../errors";
|
|
|
11
11
|
import { canHaveChildren } from "socket-function/src/types";
|
|
12
12
|
import { URLOverride } from "./ATag";
|
|
13
13
|
import { LOCAL_DOMAIN } from "../0-path-value-core/PathRouterConstants";
|
|
14
|
+
import { isValueProxy2 } from "../2-proxy/pathValueProxy";
|
|
14
15
|
|
|
15
16
|
export interface URLParam<T = unknown> {
|
|
16
17
|
value: T;
|
|
@@ -84,7 +85,9 @@ export function createURLSync<T>(urlKey: string, defaultValue: T, config?: URLPa
|
|
|
84
85
|
if (config?.storage === "localStorage") {
|
|
85
86
|
localStorageKeys.add(urlKey);
|
|
86
87
|
}
|
|
88
|
+
let defaultSet = false;
|
|
87
89
|
async function setDefaults() {
|
|
90
|
+
defaultSet = true;
|
|
88
91
|
let { Querysub } = await import("../4-querysub/Querysub");
|
|
89
92
|
Querysub.localCommit(() => {
|
|
90
93
|
data().defaults[urlKey] = defaultValue;
|
|
@@ -93,7 +96,7 @@ export function createURLSync<T>(urlKey: string, defaultValue: T, config?: URLPa
|
|
|
93
96
|
}
|
|
94
97
|
});
|
|
95
98
|
}
|
|
96
|
-
|
|
99
|
+
setImmediate(setDefaults);
|
|
97
100
|
function deleteKeys(obj: any) {
|
|
98
101
|
if (!canHaveChildren(obj)) return;
|
|
99
102
|
for (let key in obj) {
|
|
@@ -107,18 +110,23 @@ export function createURLSync<T>(urlKey: string, defaultValue: T, config?: URLPa
|
|
|
107
110
|
urlKey,
|
|
108
111
|
default: defaultValue,
|
|
109
112
|
get value() {
|
|
113
|
+
let resultValue: T | undefined = undefined;
|
|
110
114
|
if (!proxyWatcher.inWatcher()) {
|
|
111
115
|
// NOTE: This makes async functions a bit easier, although... async functions
|
|
112
116
|
// are almost never needed.
|
|
113
|
-
|
|
117
|
+
resultValue = Querysub.localRead(() => {
|
|
114
118
|
return data().params[urlKey] as T;
|
|
115
119
|
});
|
|
116
120
|
} else {
|
|
117
121
|
// Makes the access faster, by skipping schema checks
|
|
118
|
-
|
|
122
|
+
resultValue = noAtomicSchema(() => {
|
|
119
123
|
return data().params[urlKey] as T;
|
|
120
124
|
});
|
|
121
125
|
}
|
|
126
|
+
if (!defaultSet && (resultValue === undefined || isValueProxy2(resultValue))) {
|
|
127
|
+
resultValue = defaultValue;
|
|
128
|
+
}
|
|
129
|
+
return resultValue;
|
|
122
130
|
},
|
|
123
131
|
set value(value: T) {
|
|
124
132
|
let resetTargets = resetLinks.get(urlKey);
|