@yusukeshib/pi-babysit 0.6.20 → 0.6.21

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/index.ts +64 -13
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -2851,34 +2851,55 @@ const remoteTerminalTails = new Set<string>();
2851
2851
  // append-only mirror built from incremental `log --since --raw --json` reads.
2852
2852
  // The cursor records the remote raw-byte offset and the local mirror size it
2853
2853
  // corresponds to, so an interrupted append is truncated instead of duplicated.
2854
- type RemoteLogCursor = { remoteOffset: number; localBytes: number; complete: boolean };
2854
+ type RemoteLogCursor = { remoteOffset: number; localBytes: number; timestampBytes: number; complete: boolean };
2855
2855
  type RemoteLogSyncState = { inFlight?: Promise<boolean>; lastSyncAt: number; lastChanged: boolean; error?: string };
2856
2856
  const remoteLogSyncs = new Map<string, RemoteLogSyncState>();
2857
2857
  const REMOTE_LOG_SYNC_INTERVAL_MS = 1000;
2858
+ // Hosts whose babysit predates `log --timestamps`; their mirrors have no dates.
2859
+ const remoteLogTimestampUnsupported = new Set<string>();
2858
2860
 
2859
2861
  export function remoteLogMirrorPath(id: string): string {
2860
2862
  return path.join(ROOT, "remote-logs", id, "output.log");
2861
2863
  }
2862
2864
 
2865
+ function remoteLogTimestampsPath(id: string): string {
2866
+ return path.join(path.dirname(remoteLogMirrorPath(id)), "output.timestamps.jsonl");
2867
+ }
2868
+
2863
2869
  function remoteLogCursorPath(id: string): string {
2864
2870
  return path.join(path.dirname(remoteLogMirrorPath(id)), "cursor.json");
2865
2871
  }
2866
2872
 
2867
2873
  function readRemoteLogCursor(id: string): RemoteLogCursor {
2868
2874
  const file = remoteLogMirrorPath(id);
2869
- let cursor: RemoteLogCursor = { remoteOffset: 0, localBytes: 0, complete: false };
2875
+ const sidecar = remoteLogTimestampsPath(id);
2876
+ const empty: RemoteLogCursor = { remoteOffset: 0, localBytes: 0, timestampBytes: 0, complete: false };
2877
+ let cursor = empty;
2870
2878
  try {
2871
2879
  const parsed = JSON.parse(fs.readFileSync(remoteLogCursorPath(id), "utf8"));
2872
- if (Number.isSafeInteger(parsed.remoteOffset) && Number.isSafeInteger(parsed.localBytes) && parsed.remoteOffset >= 0 && parsed.localBytes >= 0) {
2873
- cursor = { remoteOffset: parsed.remoteOffset, localBytes: parsed.localBytes, complete: parsed.complete === true };
2880
+ const valid = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0;
2881
+ if (valid(parsed.remoteOffset) && valid(parsed.localBytes)) {
2882
+ cursor = { remoteOffset: parsed.remoteOffset, localBytes: parsed.localBytes, timestampBytes: valid(parsed.timestampBytes) ? parsed.timestampBytes : 0, complete: parsed.complete === true };
2874
2883
  }
2875
2884
  } catch {}
2876
- let size = 0;
2877
- try {
2878
- size = fs.statSync(file).size;
2879
- } catch {}
2880
- if (size < cursor.localBytes) return { remoteOffset: 0, localBytes: 0, complete: false };
2885
+ const sizeOf = (target: string) => {
2886
+ try {
2887
+ return fs.statSync(target).size;
2888
+ } catch {
2889
+ return 0;
2890
+ }
2891
+ };
2892
+ const size = sizeOf(file);
2893
+ const sidecarSize = sizeOf(sidecar);
2894
+ if (size < cursor.localBytes || sidecarSize < cursor.timestampBytes) {
2895
+ // Committed data is missing; restart the mirror from scratch.
2896
+ if (size > 0) fs.truncateSync(file, 0);
2897
+ if (sidecarSize > 0) fs.truncateSync(sidecar, 0);
2898
+ return empty;
2899
+ }
2900
+ // Drop an append that was interrupted before its cursor was committed.
2881
2901
  if (size > cursor.localBytes) fs.truncateSync(file, cursor.localBytes);
2902
+ if (sidecarSize > cursor.timestampBytes) fs.truncateSync(sidecar, cursor.timestampBytes);
2882
2903
  return cursor;
2883
2904
  }
2884
2905
 
@@ -2900,14 +2921,23 @@ export async function syncRemoteLog(id: string): Promise<boolean> {
2900
2921
  if (!fs.existsSync(file)) fs.writeFileSync(file, "");
2901
2922
  const cursor = readRemoteLogCursor(id);
2902
2923
  if (cursor.complete) return false;
2903
- const result = await bs(["log", "-s", id, "--since", String(cursor.remoteOffset), "--raw", "--json"], { unboundedStdout: true });
2924
+ const host = readMeta(id)?.host ?? "";
2925
+ const readArgs = ["log", "-s", id, "--since", String(cursor.remoteOffset), "--raw", "--json"];
2926
+ let withTimestamps = !remoteLogTimestampUnsupported.has(host);
2927
+ let result = await bs(withTimestamps ? [...readArgs, "--timestamps"] : readArgs, { unboundedStdout: true });
2928
+ if (withTimestamps && result.code !== 0 && /--timestamps/.test(result.stderr)) {
2929
+ // Older remote babysit: keep mirroring text, without capture dates.
2930
+ remoteLogTimestampUnsupported.add(host);
2931
+ withTimestamps = false;
2932
+ result = await bs(readArgs, { unboundedStdout: true });
2933
+ }
2904
2934
  if (result.code !== 0) {
2905
2935
  const error = clip(result.stderr.trim() || result.stdout.trim() || `babysit log exited ${result.code}`, 500);
2906
2936
  const changed = state.error !== error;
2907
2937
  state.error = error;
2908
2938
  return changed;
2909
2939
  }
2910
- let response: { text?: unknown; offset?: unknown; done?: unknown };
2940
+ let response: { text?: unknown; offset?: unknown; done?: unknown; timestamps?: unknown };
2911
2941
  try {
2912
2942
  response = JSON.parse(result.stdout);
2913
2943
  } catch {
@@ -2921,18 +2951,38 @@ export async function syncRemoteLog(id: string): Promise<boolean> {
2921
2951
  if ((response.offset as number) < cursor.remoteOffset) {
2922
2952
  // The remote log was replaced or truncated (e.g. restart); rebuild the mirror.
2923
2953
  fs.truncateSync(file, 0);
2924
- writeRemoteLogCursor(id, { remoteOffset: 0, localBytes: 0, complete: false });
2954
+ fs.rmSync(remoteLogTimestampsPath(id), { force: true });
2955
+ writeRemoteLogCursor(id, { remoteOffset: 0, localBytes: 0, timestampBytes: 0, complete: false });
2925
2956
  return true;
2926
2957
  }
2927
2958
  const hadError = state.error !== undefined;
2928
2959
  state.error = undefined;
2929
2960
  const bytes = Buffer.from(response.text, "utf8");
2930
2961
  if (bytes.length > 0) fs.appendFileSync(file, bytes);
2962
+ const remoteStart = cursor.remoteOffset;
2963
+ const remoteEnd = response.offset as number;
2964
+ let timestampBytes = cursor.timestampBytes;
2965
+ // Remote rows use remote raw offsets. They map 1:1 onto the mirror only
2966
+ // when the chunk arrived byte-for-byte; a lossy UTF-8 replacement shifts
2967
+ // offsets, so that chunk is left undated rather than mis-dated.
2968
+ if (withTimestamps && Array.isArray(response.timestamps) && bytes.length === remoteEnd - remoteStart) {
2969
+ const rows = response.timestamps.flatMap((row) => {
2970
+ if (!Array.isArray(row) || row.length !== 3 || !row.every((value) => Number.isSafeInteger(value) && value >= 0)) return [];
2971
+ const [start, length, time] = row as number[];
2972
+ const begin = Math.max(start, remoteStart);
2973
+ const end = Math.min(start + length, remoteEnd);
2974
+ return end > begin ? [`[${cursor.localBytes + begin - remoteStart},${end - begin},${time}]\n`] : [];
2975
+ }).join("");
2976
+ if (rows) {
2977
+ fs.appendFileSync(remoteLogTimestampsPath(id), rows);
2978
+ timestampBytes += Buffer.byteLength(rows);
2979
+ }
2980
+ }
2931
2981
  const advanced = (response.offset as number) > cursor.remoteOffset || bytes.length > 0;
2932
2982
  // `done` is reported before the worker's final flush, so completion
2933
2983
  // requires a terminal read that returned nothing new.
2934
2984
  const complete = response.done === true && !advanced;
2935
- writeRemoteLogCursor(id, { remoteOffset: response.offset as number, localBytes: cursor.localBytes + bytes.length, complete });
2985
+ writeRemoteLogCursor(id, { remoteOffset: remoteEnd, localBytes: cursor.localBytes + bytes.length, timestampBytes, complete });
2936
2986
  return advanced || complete || hadError;
2937
2987
  })().then((changed) => {
2938
2988
  state.lastChanged = changed;
@@ -4127,6 +4177,7 @@ export default function (pi: ExtensionAPI) {
4127
4177
  remoteTails.clear();
4128
4178
  remoteTerminalTails.clear();
4129
4179
  remoteLogSyncs.clear();
4180
+ remoteLogTimestampUnsupported.clear();
4130
4181
  pollNeeded = true;
4131
4182
  const retentionDays = Number(process.env.PI_BABYSIT_RETENTION_DAYS ?? "3");
4132
4183
  if (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.6.20",
3
+ "version": "0.6.21",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",