tesseract-agent 0.11.2 → 0.11.3

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.
@@ -37920,6 +37920,9 @@ import { spawn as spawn2, execSync, execFileSync } from "node:child_process";
37920
37920
  function setBrowserExtensionProbe(fn) {
37921
37921
  browserExtensionConnected = fn;
37922
37922
  }
37923
+ function isTurnBearingFrame(type) {
37924
+ return type === "assistant" || type === "stream_event" || type === "user";
37925
+ }
37923
37926
  function findUserInstalledClaude(binName) {
37924
37927
  const home = homedir3();
37925
37928
  const abs = [
@@ -39980,50 +39983,72 @@ ${full.slice(-8e3)}`;
39980
39983
  let cappedOut = false;
39981
39984
  let narrated = false;
39982
39985
  const turn = live.turnSeq;
39983
- for (; ; ) {
39984
- if (live.rv !== rv) {
39985
- log(`[work] quiescence wait for ${live.sessionId} dropped \u2014 another run owns the session`);
39986
- this.clearWorkNarration(live, narrated);
39987
- return;
39988
- }
39989
- if (live.reaped) {
39990
- log(`[work] quiescence wait for ${live.sessionId} dropped \u2014 the session was torn down`);
39991
- this.clearWorkNarration(live, narrated);
39992
- return;
39993
- }
39994
- if (live.turnSeq !== turn) {
39995
- log(`[work] quiescence wait for ${live.sessionId} superseded by a new turn`);
39996
- this.clearWorkNarration(live, narrated);
39997
- return;
39998
- }
39999
- const v = live.ledger.verdict(Date.now());
40000
- if (v.quiescent)
40001
- break;
40002
- if (Date.now() - started2 >= QUIESCENCE_MAX_MS) {
40003
- cappedOut = true;
40004
- break;
40005
- }
40006
- if (!narrated && live.sessionId) {
40007
- narrated = true;
40008
- this.send({
40009
- type: "agent:narration",
40010
- sessionId: live.sessionId,
40011
- text: `Waiting for ${v.waiting.length} background ${v.waiting.length === 1 ? "task" : "tasks"} to finish\u2026`,
40012
- engineLabel: "Claude Code"
40013
- }, live.origin);
39986
+ live.gradePending = true;
39987
+ try {
39988
+ for (; ; ) {
39989
+ if (live.rv !== rv) {
39990
+ log(`[work] quiescence wait for ${live.sessionId} dropped \u2014 another run owns the session`);
39991
+ this.clearWorkNarration(live, narrated);
39992
+ return;
39993
+ }
39994
+ if (live.gone) {
39995
+ log(`[work] quiescence wait for ${live.sessionId} dropped \u2014 the session was torn down`);
39996
+ this.clearWorkNarration(live, narrated);
39997
+ return;
39998
+ }
39999
+ if (live.turnSeq !== turn) {
40000
+ log(`[work] quiescence wait for ${live.sessionId} superseded by a new turn`);
40001
+ this.clearWorkNarration(live, narrated);
40002
+ return;
40003
+ }
40004
+ const v = live.ledger.verdict(Date.now());
40005
+ if (v.quiescent)
40006
+ break;
40007
+ if (Date.now() - started2 >= QUIESCENCE_MAX_MS) {
40008
+ cappedOut = true;
40009
+ break;
40010
+ }
40011
+ if (!narrated && live.sessionId) {
40012
+ narrated = true;
40013
+ this.send({
40014
+ type: "agent:narration",
40015
+ sessionId: live.sessionId,
40016
+ text: `Waiting for ${v.waiting.length} background ${v.waiting.length === 1 ? "task" : "tasks"} to finish\u2026`,
40017
+ engineLabel: "Claude Code"
40018
+ }, live.origin);
40019
+ }
40020
+ await new Promise((res) => setTimeout(res, QUIESCENCE_POLL_MS));
40014
40021
  }
40015
- await new Promise((res) => setTimeout(res, QUIESCENCE_POLL_MS));
40022
+ this.clearWorkNarration(live, narrated);
40023
+ live.ledger.sweep(Date.now());
40024
+ const outstanding = live.ledger.summarize(Date.now());
40025
+ if (cappedOut)
40026
+ log(`[work] quiescence cap reached for ${live.sessionId}; grading anyway with: ${outstanding.join("; ") || "(nothing outstanding)"}`);
40027
+ else if (Date.now() - started2 > 1e3)
40028
+ log(`[work] ${live.sessionId} settled after ${Math.round((Date.now() - started2) / 1e3)}s \u2014 grading now`);
40029
+ if (!cappedOut && outstanding.length)
40030
+ log(`[work] grading with work still in flight: ${outstanding.join("; ")}`);
40031
+ await this.runVerification(live, finalText, didWork);
40032
+ } finally {
40033
+ live.gradePending = false;
40034
+ }
40035
+ }
40036
+ /**
40037
+ * Mark a turn the engine started on its own as busy — exactly what a send does.
40038
+ *
40039
+ * Bumping `turnSeq` is deliberate: a quiescence wait for the PREVIOUS turn must see itself
40040
+ * superseded, the same way it would by a turn we started, rather than grade the old final
40041
+ * text while the new turn runs.
40042
+ */
40043
+ adoptSelfStartedTurn(live) {
40044
+ log(`[turn] ${live.sessionId || "(pending)"} started a turn on its own \u2014 marking busy`);
40045
+ live.streaming = true;
40046
+ live.turnSeq++;
40047
+ live.lastActivity = Date.now();
40048
+ if (live.idleTimer) {
40049
+ clearTimeout(live.idleTimer);
40050
+ live.idleTimer = void 0;
40016
40051
  }
40017
- this.clearWorkNarration(live, narrated);
40018
- live.ledger.sweep(Date.now());
40019
- const outstanding = live.ledger.summarize(Date.now());
40020
- if (cappedOut)
40021
- log(`[work] quiescence cap reached for ${live.sessionId}; grading anyway with: ${outstanding.join("; ") || "(nothing outstanding)"}`);
40022
- else if (Date.now() - started2 > 1e3)
40023
- log(`[work] ${live.sessionId} settled after ${Math.round((Date.now() - started2) / 1e3)}s \u2014 grading now`);
40024
- if (!cappedOut && outstanding.length)
40025
- log(`[work] grading with work still in flight: ${outstanding.join("; ")}`);
40026
- await this.runVerification(live, finalText, didWork);
40027
40052
  }
40028
40053
  /** Take down the "waiting on background work" strip, if we put one up. */
40029
40054
  clearWorkNarration(live, narrated) {
@@ -42557,6 +42582,8 @@ ${unattendedReportDirective(params)}` : ""), live.sessionId) },
42557
42582
  }
42558
42583
  const sid = live.sessionId;
42559
42584
  const parentToolUseId = message.parent_tool_use_id ?? void 0;
42585
+ if (!live.streaming && !live.reaped && isTurnBearingFrame(message.type))
42586
+ this.adoptSelfStartedTurn(live);
42560
42587
  switch (message.type) {
42561
42588
  case "stream_event": {
42562
42589
  const event = message.event;
@@ -43314,6 +43341,7 @@ ${err2.stack}` : ""}` : String(err2);
43314
43341
  emit2({ type: "agent:status", sessionId: sid, status: "error" });
43315
43342
  }
43316
43343
  } finally {
43344
+ live.gone = true;
43317
43345
  if (live.idleTimer) {
43318
43346
  clearTimeout(live.idleTimer);
43319
43347
  live.idleTimer = void 0;
@@ -43522,6 +43550,7 @@ ${err2.stack}` : ""}` : String(err2);
43522
43550
  live.abort.abort();
43523
43551
  } catch {
43524
43552
  }
43553
+ live.gone = true;
43525
43554
  }
43526
43555
  /** M7 — stop ONE background task (vs interruptSession which aborts the whole
43527
43556
  * turn). Driver-only, same origin guard. No-op if the session/task is gone. */
@@ -44247,12 +44276,45 @@ ${lines.join("\n")}
44247
44276
  /** Close an idle session's Query (→ child process + IPC freed via runConsumer's
44248
44277
  * finally), keeping the on-disk session resumable. Deletes from the live map
44249
44278
  * FIRST so a racing sendMessage misses it and cleanly re-creates with resume. */
44279
+ /**
44280
+ * Is this session, whatever `streaming` says, still in the middle of something?
44281
+ *
44282
+ * `streaming` only tracks turns WE started. A CLI running its own background monitor
44283
+ * starts turns by itself when events land, and between two of those — or during a long
44284
+ * tool call inside one — the session reads as idle here while the engine is hard at work.
44285
+ * The memory monitor's moderate-pressure reclaim reaped exactly such a session, 60 seconds
44286
+ * "idle" with a batch barrier that had just reported "1 still running": the CLI carried on
44287
+ * for another 69 minutes, un-mapped and ungradeable. Work the ledger is awaiting, a job
44288
+ * that is running, a wait inside `await_jobs`, or a turn-end grade still waiting for
44289
+ * quiescence — any of these means the session is busy.
44290
+ */
44291
+ hasWorkInFlight(live) {
44292
+ if (live.gradePending)
44293
+ return true;
44294
+ if ((live.awaitDepth ?? 0) > 0)
44295
+ return true;
44296
+ try {
44297
+ if (live.ledger.verdict(Date.now()).waiting.length > 0)
44298
+ return true;
44299
+ } catch {
44300
+ }
44301
+ try {
44302
+ if (live.jobs?.list().some((j) => j.status === "running"))
44303
+ return true;
44304
+ } catch {
44305
+ }
44306
+ return false;
44307
+ }
44250
44308
  reapIdleSession(sessionId, force = false) {
44251
44309
  const live = this.liveSessions.get(sessionId);
44252
44310
  if (!live || live.streaming || live.reaped)
44253
44311
  return false;
44254
44312
  if (this.isHeld(live) && !force)
44255
44313
  return false;
44314
+ if (!force && this.hasWorkInFlight(live)) {
44315
+ log(`[reap] ${sessionId} looks idle but has work in flight \u2014 left alone`);
44316
+ return false;
44317
+ }
44256
44318
  log("Reaping idle session:", sessionId);
44257
44319
  live.reaped = true;
44258
44320
  this.emitLifecycle("SessionEnd", { sessionId, reason: force ? "forced" : "idle-reap" });
@@ -44301,6 +44363,10 @@ ${lines.join("\n")}
44301
44363
  let n = 0;
44302
44364
  for (const live of [...this.liveSessions.values()]) {
44303
44365
  if (!live.streaming && !live.reaped) {
44366
+ if (this.hasWorkInFlight(live)) {
44367
+ log(`[reap] critical reclaim spared ${live.sessionId}: work in flight`);
44368
+ continue;
44369
+ }
44304
44370
  if (this.reapIdleSession(live.sessionId, true))
44305
44371
  n++;
44306
44372
  }
@@ -44325,6 +44391,7 @@ ${lines.join("\n")}
44325
44391
  if (live.reaped)
44326
44392
  continue;
44327
44393
  live.reaped = true;
44394
+ live.gone = true;
44328
44395
  if (live.idleTimer) {
44329
44396
  clearTimeout(live.idleTimer);
44330
44397
  live.idleTimer = void 0;
@@ -110149,7 +110216,7 @@ var NetdEngine = class {
110149
110216
  p.relayed = p.relayed || relayed;
110150
110217
  this.#pendingEndpoints.set(k, p);
110151
110218
  }
110152
- startHandshake(staticPub, _nowMs) {
110219
+ #flushHint(staticPub) {
110153
110220
  const k = staticPub.toString("hex");
110154
110221
  const p = this.#pendingEndpoints.get(k) ?? { endpoints: [], relayed: false };
110155
110222
  this.#pendingEndpoints.delete(k);
@@ -110158,6 +110225,12 @@ var NetdEngine = class {
110158
110225
  this.#lastError = `hint: ${r.detail}`;
110159
110226
  });
110160
110227
  }
110228
+ startHandshake(staticPub, _nowMs) {
110229
+ this.#flushHint(staticPub);
110230
+ }
110231
+ ensureHandshake(staticPub, _nowMs) {
110232
+ this.#flushHint(staticPub);
110233
+ }
110161
110234
  /** No UDP here — netd owns the socket. Never called in practice; kept for the interface. */
110162
110235
  injectUdp(_pkt, _from, _nowMs) {
110163
110236
  }
@@ -111066,7 +111139,7 @@ var MeshNode = class {
111066
111139
  this.#peers.set(hex6, staticPub);
111067
111140
  }
111068
111141
  engine.addPath(staticPub, `${address}:${port}`, false);
111069
- engine.startHandshake(staticPub, Date.now());
111142
+ engine.ensureHandshake(staticPub, Date.now());
111070
111143
  this.#drain();
111071
111144
  } catch (e) {
111072
111145
  this.#lastError = e instanceof Error ? e.message : String(e);
@@ -111151,7 +111224,7 @@ var MeshNode = class {
111151
111224
  } catch {
111152
111225
  }
111153
111226
  }
111154
- engine.startHandshake(pk, Date.now());
111227
+ engine.ensureHandshake(pk, Date.now());
111155
111228
  break;
111156
111229
  }
111157
111230
  case "mesh:gone": {
@@ -111295,7 +111368,7 @@ var MeshNode = class {
111295
111368
  for (const e of p.endpoints)
111296
111369
  engine.addPath(pk, `${p.observedIp}:${e.port}`, false);
111297
111370
  }
111298
- engine.startHandshake(pk, Date.now());
111371
+ engine.ensureHandshake(pk, Date.now());
111299
111372
  const meshId = this.#engine?.meshId();
111300
111373
  try {
111301
111374
  if (meshId)
@@ -59,6 +59,9 @@ export declare class MeshEngine {
59
59
  /** A relayed path always ranks below a direct one, regardless of RTT. */
60
60
  addPath(staticPub: Buffer, endpoint: Endpoint, relayed: boolean): void;
61
61
  startHandshake(staticPub: Buffer, nowMs: number): void;
62
+ /** Idempotent connect nudge: no-op over a live session; initiates only on first contact
63
+ * or a stale attempt. What a host calls when it (re)learns a peer. */
64
+ ensureHandshake(staticPub: Buffer, nowMs: number): void;
62
65
 
63
66
  injectTun(pkt: Buffer, nowMs: number): void;
64
67
  injectUdp(pkt: Buffer, from: Endpoint, nowMs: number): void;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fineguide-tesseract/mesh-core",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tesseract-agent",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "license": "UNLICENSED",
5
5
  "publishConfig": {
6
6
  "access": "public",