@lsctech/polaris 0.4.0 → 0.4.1

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.
@@ -110,6 +110,26 @@ function resolveTelemetryFile(state, repoRoot) {
110
110
  const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
111
111
  return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
112
112
  }
113
+ function estimateTokensFromBytes(bytes) {
114
+ return Math.round(bytes / 4);
115
+ }
116
+ function emitBootstrapContextSize(telemetryFile, runId, childId, stateFile, packet) {
117
+ const stateFileBytes = (0, node_fs_1.statSync)(stateFile).size;
118
+ const bootstrapPacketBytes = Buffer.byteLength(JSON.stringify(packet), "utf-8");
119
+ const stateEstimatedTokens = estimateTokensFromBytes(stateFileBytes);
120
+ const bootstrapEstimatedTokens = estimateTokensFromBytes(bootstrapPacketBytes);
121
+ appendTelemetry(telemetryFile, {
122
+ event: "bootstrap-context-size",
123
+ run_id: runId,
124
+ child_id: childId,
125
+ state_file_bytes: stateFileBytes,
126
+ state_estimated_tokens: stateEstimatedTokens,
127
+ bootstrap_packet_bytes: bootstrapPacketBytes,
128
+ bootstrap_estimated_tokens: bootstrapEstimatedTokens,
129
+ combined_estimated_tokens: stateEstimatedTokens + bootstrapEstimatedTokens,
130
+ timestamp: new Date().toISOString(),
131
+ });
132
+ }
113
133
  /**
114
134
  * Select the next open child that is not Done/blocked.
115
135
  * Returns null when all children are completed.
@@ -300,6 +320,74 @@ function buildPacket(state, activeChild, stateFile, telemetryFile, repoRoot, res
300
320
  function absoluteResultFile(repoRoot, filePath) {
301
321
  return (0, node_path_1.isAbsolute)(filePath) ? filePath : (0, node_path_1.resolve)(repoRoot, filePath);
302
322
  }
323
+ /**
324
+ * Flush bodies from open_children_meta to the durable clusters.json snapshot.
325
+ * Called once before the dispatch loop so that writeStateAtomic (which strips
326
+ * body from non-next children) does not cause data loss when the loop reloads
327
+ * state from disk between dispatches.
328
+ *
329
+ * Only writes nodes that have a body in open_children_meta but lack one in the
330
+ * existing snapshot. Never removes or overwrites existing body data.
331
+ * Never throws.
332
+ */
333
+ function flushBodiesToClusterSnapshot(state, repoRoot) {
334
+ const meta = state.open_children_meta;
335
+ if (!meta)
336
+ return;
337
+ const snapshotPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "clusters.json");
338
+ let snapshot = {};
339
+ try {
340
+ const raw = (0, node_fs_1.readFileSync)(snapshotPath, "utf-8");
341
+ snapshot = JSON.parse(raw);
342
+ }
343
+ catch (err) {
344
+ // Distinguish "file not found" from "present but corrupted"
345
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
346
+ // File absent — start fresh
347
+ snapshot = {
348
+ schemaVersion: "v2",
349
+ source: { id: state.cluster_id, type: "local" },
350
+ nodes: {},
351
+ dependencies: {},
352
+ clusters: { [state.cluster_id]: { id: state.cluster_id, title: state.cluster_id, children: state.open_children } },
353
+ activeCluster: state.cluster_id,
354
+ };
355
+ }
356
+ else {
357
+ // File present but corrupted — do not overwrite
358
+ return;
359
+ }
360
+ }
361
+ if (typeof snapshot.nodes !== "object" || snapshot.nodes === null) {
362
+ snapshot.nodes = {};
363
+ }
364
+ let changed = false;
365
+ for (const [id, childMeta] of Object.entries(meta)) {
366
+ const body = childMeta?.body;
367
+ if (!body || body.trim().length === 0)
368
+ continue;
369
+ const existing = snapshot.nodes[id];
370
+ if (existing && existing.body && existing.body.trim().length > 0)
371
+ continue;
372
+ snapshot.nodes[id] = {
373
+ ...(existing ?? {}),
374
+ id,
375
+ title: childMeta.title ?? existing?.title ?? id,
376
+ body,
377
+ status: existing?.status ?? "Todo",
378
+ };
379
+ changed = true;
380
+ }
381
+ if (!changed)
382
+ return;
383
+ try {
384
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(snapshotPath), { recursive: true });
385
+ (0, node_fs_1.writeFileSync)(snapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
386
+ }
387
+ catch {
388
+ // Best-effort — do not fail the loop if snapshot write fails
389
+ }
390
+ }
303
391
  function buildClusterArtifactPaths(repoRoot, clusterId, childId, dispatchId) {
304
392
  const clusterDir = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId);
305
393
  const filename = `${childId}-${dispatchId}.json`;
@@ -629,6 +717,14 @@ async function runParentLoop(options) {
629
717
  }
630
718
  }
631
719
  let childrenDispatched = 0;
720
+ // ── Flush bodies to clusters.json before stripping begins ──────────────
721
+ // writeStateAtomic strips body from all open_children_meta entries except
722
+ // open_children[0]. Flush any bodies present in open_children_meta to the
723
+ // durable cluster snapshot so readBodyFromClusterSnapshot can hydrate them
724
+ // on subsequent iterations after reload from disk.
725
+ if (!dryRun) {
726
+ flushBodiesToClusterSnapshot(state, repoRoot);
727
+ }
632
728
  // ── Lifecycle manager: enforce one-active-worker policy ─────────────────
633
729
  // forceReleaseAll() clears any orphaned registrations from a previous
634
730
  // crashed session. Registrations are session-memory only; they are not
@@ -986,6 +1082,7 @@ async function runParentLoop(options) {
986
1082
  dry_run: dryRun,
987
1083
  timestamp: new Date().toISOString(),
988
1084
  });
1085
+ emitBootstrapContextSize(telemetryFile, state.run_id, nextChild, stateFile, packet);
989
1086
  }
990
1087
  let dispatchResult;
991
1088
  try {
@@ -1284,6 +1381,10 @@ async function runParentLoop(options) {
1284
1381
  else {
1285
1382
  state = reloadedState;
1286
1383
  reloadedStateValid = true;
1384
+ // Flush bodies from worker-updated state to clusters.json before any stripping
1385
+ if (!dryRun) {
1386
+ flushBodiesToClusterSnapshot(state, repoRoot);
1387
+ }
1287
1388
  }
1288
1389
  }
1289
1390
  catch (err) {
@@ -1410,9 +1511,28 @@ async function runParentLoop(options) {
1410
1511
  }
1411
1512
  }
1412
1513
  // workerStatus === "done" has already been validated upstream.
1514
+ // Build the durable Role Evidence Contract for this completed child.
1515
+ const nextRecommendedAction = summaryAsRecord?.['next_recommended_action'] === 'continue' ||
1516
+ summaryAsRecord?.['next_recommended_action'] === 'stop' ||
1517
+ summaryAsRecord?.['next_recommended_action'] === 'investigate'
1518
+ ? summaryAsRecord['next_recommended_action']
1519
+ : 'continue';
1520
+ const workerResult = (0, checkpoint_js_1.buildWorkerResultContract)({
1521
+ state,
1522
+ childId: nextChild,
1523
+ resultFile: packet.result_file_contract.result_file,
1524
+ telemetryFile,
1525
+ lastCommit: lastCommit ?? null,
1526
+ validation: validationSummary,
1527
+ packetHash: (0, checkpoint_js_1.computePacketHashFromPath)(packetPath),
1528
+ status: 'done',
1529
+ nextRecommendedAction,
1530
+ resultData: summaryAsRecord?.['result_data'],
1531
+ });
1413
1532
  // If the reloaded state already reflects the completed child,
1414
1533
  // the worker owns the completion checkpoint and the parent
1415
- // must not rewrite it.
1534
+ // must not rewrite the open/closed lists, but it still records the
1535
+ // Role Evidence Contract so scoring can consume it later.
1416
1536
  if (!workerWroteCompletion) {
1417
1537
  // ── Dispatch boundary: verify dispatch happened before advancing ──────
1418
1538
  // The parent dispatched via adapter, so dispatch_boundary should show
@@ -1444,6 +1564,10 @@ async function runParentLoop(options) {
1444
1564
  state = {
1445
1565
  ...advanced,
1446
1566
  dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
1567
+ completed_children_results: {
1568
+ ...advanced.completed_children_results,
1569
+ [nextChild]: workerResult,
1570
+ },
1447
1571
  };
1448
1572
  childrenDispatched += 1;
1449
1573
  // Worker did not write its own completion — orchestrator fills the gap.
@@ -1457,6 +1581,10 @@ async function runParentLoop(options) {
1457
1581
  state = {
1458
1582
  ...state,
1459
1583
  dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
1584
+ completed_children_results: {
1585
+ ...state.completed_children_results,
1586
+ [nextChild]: workerResult,
1587
+ },
1460
1588
  };
1461
1589
  childrenDispatched += 1;
1462
1590
  if (!dryRun) {
@@ -61,7 +61,7 @@ const OPEN_CHILD_STATUSES = new Set([
61
61
  ]);
62
62
  function findClusterStateForPacket(repoRoot, packet) {
63
63
  const clustersDir = (0, node_path_1.resolve)(repoRoot, ".polaris", "clusters");
64
- const hints = new Set([packet.last_completed_child, ...packet.open_children].filter((value) => typeof value === "string" && value.length > 0));
64
+ const hints = new Set([packet.last_completed_child, packet.open_children.next_child].filter((value) => typeof value === "string" && value.length > 0));
65
65
  let entries;
66
66
  try {
67
67
  entries = (0, node_fs_1.readdirSync)(clustersDir, { withFileTypes: true })
@@ -96,12 +96,16 @@ function rebuildLoopStateFromClusterState(packet, clusterState, repoRoot) {
96
96
  const completedChildren = clusterState.child_states
97
97
  .filter((child) => COMPLETED_CHILD_STATUSES.has(child.status))
98
98
  .map((child) => child.id);
99
- const openChildren = packet.open_children.filter((childId) => clusterChildren.has(childId));
100
- const fallbackOpenChildren = openChildren.length > 0
101
- ? openChildren
102
- : clusterState.child_states
103
- .filter((child) => OPEN_CHILD_STATUSES.has(child.status))
104
- .map((child) => child.id);
99
+ // Reconstruct full open queue from all open child_states
100
+ const allOpenChildren = clusterState.child_states
101
+ .filter((child) => OPEN_CHILD_STATUSES.has(child.status))
102
+ .map((child) => child.id);
103
+ // Place next_child first if present, then append remaining open children
104
+ const nextChild = packet.open_children.next_child;
105
+ const openChildren = nextChild && clusterChildren.has(nextChild)
106
+ ? [nextChild, ...allOpenChildren.filter((id) => id !== nextChild)]
107
+ : allOpenChildren;
108
+ const fallbackOpenChildren = openChildren;
105
109
  const activeChild = fallbackOpenChildren.find((childId) => {
106
110
  const status = clusterState.child_states.find((child) => child.id === childId)?.status;
107
111
  return status === "claimed" || status === "dispatched" || status === "running";
@@ -177,7 +181,9 @@ function appendResumedLedgerEvent(repoRoot, packet, state) {
177
181
  : [];
178
182
  const openChildren = Array.isArray(state.open_children)
179
183
  ? state.open_children
180
- : packet.open_children;
184
+ : packet.open_children.next_child
185
+ ? [packet.open_children.next_child]
186
+ : [];
181
187
  new ledger_js_1.LedgerWriter((0, node_path_1.join)(repoRoot, ledger_js_1.DEFAULT_LEDGER_PATH)).append({
182
188
  schema_version: 1,
183
189
  event_id: (0, node_crypto_1.randomUUID)(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsctech/polaris",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Polaris — standalone taskchain orchestration framework for governed AI agent workflows",
5
5
  "bin": {
6
6
  "polaris": "dist/cli/index.js",