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.
@@ -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
- public async setPatchStatus(id: string, commentId: string, status: TicketPatchStatus): Promise<void> {
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
- let comment = ticket.comments.find(c => c.id === commentId);
113
- if (!comment) {
114
- throw new Error(`Comment ${commentId} not found in ticket ${id}`);
115
- }
116
- if (comment.kind !== "patch") {
117
- throw new Error(`Comment ${commentId} is not a patch comment`);
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 does the filesystem work. On success we record the applied status.
125
- public async applyPatch(id: string, commentId: string): Promise<void> {
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].applyPatch(id, commentId);
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 the patch cannot be applied. Run \`yarn autofix\` in the repository the patch targets.`);
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 patch on all connected autofixer nodes:\n${errors.join("\n")}`);
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
- setPatchStatus: {},
194
- applyPatch: {},
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 setPatchStatus(id: string, commentId: string, status: TicketPatchStatus): Promise<void> {
248
- await TicketServiceBase.nodes[await getTicketServiceNode()].setPatchStatus(id, commentId, status);
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 applyPatch(id: string, commentId: string): Promise<void> {
252
- await TicketServiceBase.nodes[await getTicketServiceNode()].applyPatch(id, commentId);
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
- setPatchStatus: {},
316
- applyPatch: {},
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: "Rejected reruns", emoji: "⚠️",
144
+ perFn, category: "Calls", title: "Max rejected reruns (one call)", emoji: "⚠️",
146
145
  threshold: 10,
147
- getFnValue: fn => fn.totalFullReruns,
146
+ getFnValue: fn => fn.maxFullReruns,
148
147
  }),
149
148
  makeCallStatSync({
150
- perFn, category: "Calls", title: "Rerun to sync data", emoji: "♻️",
149
+ perFn, category: "Calls", title: "Max reruns to sync data (one call)", emoji: "♻️",
151
150
  threshold: 10,
152
- getFnValue: fn => fn.totalInternalReruns,
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: "Average evaluation time", emoji: "💫",
157
+ perFn, category: "Timing", title: "Max evaluation time (one call)", emoji: "💫",
169
158
  threshold: 100,
170
- getFnValue: fn => avgFn(fn.evalTimeStats),
159
+ getFnValue: fn => fn.maxEvalTime,
171
160
  formatValue: formatTime,
172
161
  }),
173
162
  makeCallStatSync({
174
- perFn, category: "Timing", title: "Average full time (sync, etc)", emoji: "🕒",
163
+ perFn, category: "Timing", title: "Max full time (sync, etc, one call)", emoji: "🕒",
175
164
  threshold: 1000,
176
- getFnValue: fn => avgFn(fn.totalTimeStats),
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
- setTimeout(setDefaults);
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
- return Querysub.localRead(() => {
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
- return noAtomicSchema(() => {
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);