@spexcode/spec-cli 0.7.0-next.1 → 0.7.0-next.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.
Files changed (2) hide show
  1. package/dist/sessions.js +77 -6
  2. package/package.json +7 -7
package/dist/sessions.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFile, execFileSync, spawn } from 'node:child_process';
2
+ import { createConnection } from 'node:net';
2
3
  import { promisify } from 'node:util';
3
4
  import { createHash, randomUUID } from 'node:crypto';
4
5
  import { readFileSync, writeFileSync, existsSync, renameSync, linkSync, mkdirSync, rmSync, readdirSync, realpathSync, statSync, unlinkSync } from 'node:fs';
@@ -15,6 +16,7 @@ import { readSessionFiles } from './session-files.js';
15
16
  import { readSessionWebs } from './session-web.js';
16
17
  import { acquireFreshSessionApplicationForCreate, configuredSessionApplicationIfCutover, initializeFreshSessionApplication, releaseFreshSessionApplicationForCreate, sessionApplicationCutoverState, setSessionApplicationCommitWake } from './session-application.js';
17
18
  import { jsonMigrationFencePath } from '@spexcode/session-application';
19
+ import { decodeEventJson } from '@spexcode/session-events';
18
20
  import { withDeliveryLocks } from './delivery-lock.js';
19
21
  import { withSessionRecordLock, withSessionRecordLockSync as coreWithSessionRecordLockSync } from './session-record-lock.js';
20
22
  import { stripRefSigil } from './mentions.js';
@@ -1704,11 +1706,45 @@ let draining = false; // re-entrancy guard: only one drain pass runs at a time (
1704
1706
  // A native receipt is bound before the readiness fence validates it. Suppress only that immediate wake so
1705
1707
  // queued prompts cannot drain during the candidate window; the successful publication path drains normally.
1706
1708
  const readinessWakeSuppressed = new Set();
1709
+ // A readiness timeout is launch-phase evidence only until the session produces another durable event. The
1710
+ // first event at/after the readiness marker is the launch transition itself; anything after that proves the
1711
+ // worker progressed (including a declaration), so a late diagnostic is moot and must not replace its note.
1712
+ function hasLaterLaunchReadinessEvent(rec) {
1713
+ const startedAt = rec.launchReadinessStartedAt;
1714
+ if (!Number.isFinite(startedAt))
1715
+ return false;
1716
+ const application = configuredSessionApplicationIfCutover();
1717
+ if (!application?.readState(rec.session))
1718
+ return false;
1719
+ const events = application.readEvents(rec.session);
1720
+ const statusPayload = (event) => {
1721
+ const payload = decodeEventJson(event.payload);
1722
+ return payload && typeof payload === 'object' && !Array.isArray(payload) && 'status' in payload
1723
+ ? payload : null;
1724
+ };
1725
+ const baseline = events.find((event) => {
1726
+ if (event.occurredAtMs < Number(startedAt))
1727
+ return false;
1728
+ const payload = statusPayload(event);
1729
+ return payload?.status === 'active';
1730
+ });
1731
+ return baseline ? events.some((event) => {
1732
+ if (event.eventSeq <= baseline.eventSeq)
1733
+ return false;
1734
+ const payload = statusPayload(event);
1735
+ if (!payload)
1736
+ return false;
1737
+ const note = payload.note;
1738
+ return !(typeof note === 'string' && /^(?:queued launch readiness failed|launch readiness warning):/.test(note));
1739
+ }) : false;
1740
+ }
1707
1741
  function noteQueuedLaunchFailureUnlocked(id, error, terminal = true, label, live = false) {
1742
+ const rec = readRecord(id);
1743
+ if (rec && (terminal || label === 'launch readiness warning') && hasLaterLaunchReadinessEvent(rec))
1744
+ return;
1708
1745
  const reason = error instanceof Error ? error.message : String(error);
1709
1746
  const note = `${label ?? (terminal ? 'queued launch readiness failed' : 'launch readiness warning')}: ${reason}`;
1710
1747
  console.error(`spex: session ${id}: ${note}`);
1711
- const rec = readRecord(id);
1712
1748
  if (rec && !retirementReason(rec) && (rec.note !== note
1713
1749
  || (terminal && (rec.status !== 'error' || !rec.stopped || rec.launchReadinessStartedAt != null))
1714
1750
  || (!terminal && live && (rec.status === 'error' || rec.stopped)))) {
@@ -2488,9 +2524,46 @@ function isExplicitConnectionRefused(error) {
2488
2524
  return errors.length > 0 && errors.every(isExplicitConnectionRefused);
2489
2525
  return isExplicitConnectionRefused(error.cause);
2490
2526
  }
2527
+ async function establishBackendConnection(target) {
2528
+ const parsed = new URL(target.url);
2529
+ const port = Number(parsed.port) || (parsed.protocol === 'https:' ? 443 : 80);
2530
+ return await new Promise((resolve, reject) => {
2531
+ const socket = createConnection({ host: parsed.hostname, port });
2532
+ let settled = false;
2533
+ const finish = (fn) => {
2534
+ if (settled)
2535
+ return;
2536
+ settled = true;
2537
+ clearTimeout(timer);
2538
+ socket.destroy();
2539
+ fn();
2540
+ };
2541
+ const timer = setTimeout(() => finish(() => {
2542
+ const error = new Error(`backend connection was not accepted at ${target.url} within 1500ms`);
2543
+ error.name = 'BackendError';
2544
+ Object.assign(error, { code: 'backend_availability_indeterminate' });
2545
+ reject(error);
2546
+ }), 1500);
2547
+ timer.unref?.();
2548
+ socket.once('connect', () => finish(() => resolve(false)));
2549
+ socket.once('error', (error) => finish(() => {
2550
+ if (isExplicitConnectionRefused(error))
2551
+ return resolve(true);
2552
+ const failed = new Error(`backend availability is indeterminate at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
2553
+ failed.name = 'BackendError';
2554
+ Object.assign(failed, { code: 'backend_availability_indeterminate', cause: error });
2555
+ reject(failed);
2556
+ }));
2557
+ });
2558
+ }
2491
2559
  async function probeSessionCreateAuthority(target) {
2560
+ // TCP acceptance establishes presence. The identity route can be delayed by a busy backend event loop,
2561
+ // so it gets the ordinary create request deadline instead of a short availability deadline.
2562
+ const refused = await establishBackendConnection(target);
2563
+ if (refused)
2564
+ return true;
2492
2565
  const controller = new AbortController();
2493
- const timer = setTimeout(() => controller.abort(), 1500);
2566
+ const timer = setTimeout(() => controller.abort(new Error('backend authority request timed out')), sessionCreateTimeoutMs() + 5_000);
2494
2567
  timer.unref?.();
2495
2568
  let response;
2496
2569
  try {
@@ -2498,11 +2571,9 @@ async function probeSessionCreateAuthority(target) {
2498
2571
  }
2499
2572
  catch (error) {
2500
2573
  clearTimeout(timer);
2501
- if (isExplicitConnectionRefused(error))
2502
- return true;
2503
- const failed = new Error(`backend availability is indeterminate at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
2574
+ const failed = new Error(`backend authority read failed after connection at ${target.url}; refusing in-process session creation (${error instanceof Error ? error.message : error})`);
2504
2575
  failed.name = 'BackendError';
2505
- Object.assign(failed, { code: 'backend_availability_indeterminate', cause: error });
2576
+ Object.assign(failed, { code: 'backend_authority_read_failed', cause: error });
2506
2577
  throw failed;
2507
2578
  }
2508
2579
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-cli",
3
- "version": "0.7.0-next.1",
3
+ "version": "0.7.0-next.3",
4
4
  "type": "module",
5
5
  "description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
6
6
  "bin": {
@@ -35,12 +35,12 @@
35
35
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@spexcode/session-application": "0.7.0-next.1",
39
- "@spexcode/session-selflaunch": "0.7.0-next.1",
40
- "@spexcode/spec-core": "0.7.0-next.1",
41
- "@spexcode/spec-eval": "0.7.0-next.1",
42
- "@spexcode/spec-forge": "0.7.0-next.1",
43
- "@spexcode/transcript": "0.7.0-next.1",
38
+ "@spexcode/session-application": "0.7.0-next.3",
39
+ "@spexcode/session-selflaunch": "0.7.0-next.3",
40
+ "@spexcode/spec-core": "0.7.0-next.3",
41
+ "@spexcode/spec-eval": "0.7.0-next.3",
42
+ "@spexcode/spec-forge": "0.7.0-next.3",
43
+ "@spexcode/transcript": "0.7.0-next.3",
44
44
  "smol-toml": "^1.8.0"
45
45
  },
46
46
  "devDependencies": {