@indigoai-us/hq-cloud 6.11.15 → 6.11.16
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.
- package/.github/workflows/publish.yml +20 -64
- package/dist/agent-codex-instructions.d.ts +84 -0
- package/dist/agent-codex-instructions.d.ts.map +1 -0
- package/dist/agent-codex-instructions.js +275 -0
- package/dist/agent-codex-instructions.js.map +1 -0
- package/dist/agent-codex-instructions.test.d.ts +2 -0
- package/dist/agent-codex-instructions.test.d.ts.map +1 -0
- package/dist/agent-codex-instructions.test.js +271 -0
- package/dist/agent-codex-instructions.test.js.map +1 -0
- package/dist/bin/sync-runner-planning.d.ts +11 -0
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +19 -2
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +11 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +39 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +328 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/context.d.ts +8 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +16 -6
- package/dist/context.js.map +1 -1
- package/dist/skill-telemetry.d.ts +15 -0
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +34 -3
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +75 -1
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/vault-client.d.ts +16 -0
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +21 -0
- package/dist/vault-client.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-codex-instructions.test.ts +332 -0
- package/src/agent-codex-instructions.ts +309 -0
- package/src/bin/sync-runner-planning.ts +34 -2
- package/src/bin/sync-runner.test.ts +413 -0
- package/src/bin/sync-runner.ts +53 -0
- package/src/context.ts +17 -6
- package/src/skill-telemetry.test.ts +94 -0
- package/src/skill-telemetry.ts +51 -3
- package/src/vault-client.ts +24 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
codexRowTurnId,
|
|
10
10
|
parseCodexSessionMeta,
|
|
11
11
|
collectAndSendSkillTelemetry,
|
|
12
|
+
readFileRegion,
|
|
12
13
|
} from "./skill-telemetry.js";
|
|
13
14
|
import type { SkillInvocationBatch } from "./vault-client.js";
|
|
14
15
|
|
|
@@ -1157,3 +1158,96 @@ describe("collectAndSendSkillTelemetry — companyUid attribution", () => {
|
|
|
1157
1158
|
await fs.rm(tmp, { recursive: true, force: true });
|
|
1158
1159
|
});
|
|
1159
1160
|
});
|
|
1161
|
+
|
|
1162
|
+
describe("readFileRegion — HQ-SYNC-WEB-15 (SIGABRT on >2GiB single read)", () => {
|
|
1163
|
+
const INT32_MAX = 2 ** 31 - 1;
|
|
1164
|
+
|
|
1165
|
+
it("never issues a single read whose length exceeds Int32, even for a multi-GiB region", async () => {
|
|
1166
|
+
// The original code did `fh.read(buf, 0, currentSize - offset, offset)` in
|
|
1167
|
+
// one shot. Once a session log's unread tail passed ~2 GiB, the length
|
|
1168
|
+
// argument was no longer an Int32, so Node's native fs.read asserted
|
|
1169
|
+
// (`args[3]->IsInt32()`) and SIGABRT'd the whole runner. Simulate a 3 GiB
|
|
1170
|
+
// region with a recording fake handle (no real allocation) and prove every
|
|
1171
|
+
// native read length stays within Int32.
|
|
1172
|
+
const THREE_GIB = 3 * 1024 * 1024 * 1024;
|
|
1173
|
+
const lengths: number[] = [];
|
|
1174
|
+
let cursor = 0;
|
|
1175
|
+
const fakeFh = {
|
|
1176
|
+
read: vi.fn(
|
|
1177
|
+
async (_buf: Buffer, _off: number, length: number, position: number) => {
|
|
1178
|
+
lengths.push(length);
|
|
1179
|
+
expect(position).toBe(cursor); // contiguous, in order
|
|
1180
|
+
cursor += length;
|
|
1181
|
+
return { bytesRead: length };
|
|
1182
|
+
},
|
|
1183
|
+
),
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
const total = await readFileRegion(
|
|
1187
|
+
fakeFh as unknown as Parameters<typeof readFileRegion>[0],
|
|
1188
|
+
Buffer.alloc(0), // recording fake never touches the buffer
|
|
1189
|
+
0,
|
|
1190
|
+
THREE_GIB,
|
|
1191
|
+
);
|
|
1192
|
+
|
|
1193
|
+
expect(total).toBe(THREE_GIB);
|
|
1194
|
+
expect(lengths.length).toBeGreaterThan(1); // it actually chunked
|
|
1195
|
+
for (const len of lengths) {
|
|
1196
|
+
expect(len).toBeGreaterThan(0);
|
|
1197
|
+
expect(len).toBeLessThanOrEqual(INT32_MAX);
|
|
1198
|
+
}
|
|
1199
|
+
expect(lengths.reduce((a, b) => a + b, 0)).toBe(THREE_GIB);
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it("reads an entire real region correctly across multiple chunks", async () => {
|
|
1203
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "readregion-"));
|
|
1204
|
+
const filePath = path.join(tmp, "data.bin");
|
|
1205
|
+
const payload = Buffer.from("abcdefghijklmnopqrstuvwxyz0123456789", "utf-8");
|
|
1206
|
+
await fs.writeFile(filePath, payload);
|
|
1207
|
+
|
|
1208
|
+
const fh = await fs.open(filePath, "r");
|
|
1209
|
+
try {
|
|
1210
|
+
const buf = Buffer.alloc(payload.length);
|
|
1211
|
+
// Tiny chunk size forces the loop to iterate many times.
|
|
1212
|
+
const read = await readFileRegion(fh, buf, 0, payload.length, 7);
|
|
1213
|
+
expect(read).toBe(payload.length);
|
|
1214
|
+
expect(buf.equals(payload)).toBe(true);
|
|
1215
|
+
} finally {
|
|
1216
|
+
await fh.close();
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Reading a sub-region from an offset also lands correctly.
|
|
1220
|
+
const fh2 = await fs.open(filePath, "r");
|
|
1221
|
+
try {
|
|
1222
|
+
const buf = Buffer.alloc(10);
|
|
1223
|
+
const read = await readFileRegion(fh2, buf, 5, 10, 4);
|
|
1224
|
+
expect(read).toBe(10);
|
|
1225
|
+
expect(buf.toString("utf-8")).toBe(payload.toString("utf-8", 5, 15));
|
|
1226
|
+
} finally {
|
|
1227
|
+
await fh2.close();
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
it("stops cleanly on a short/zero read (file truncated since stat)", async () => {
|
|
1234
|
+
// Ask for 100 bytes but the file only holds 30: the handle yields bytes up
|
|
1235
|
+
// to what's left, then signals EOF with a 0-byte read.
|
|
1236
|
+
let remaining = 30;
|
|
1237
|
+
const fakeFh = {
|
|
1238
|
+
read: vi.fn(async (_b: Buffer, _o: number, length: number) => {
|
|
1239
|
+
const n = Math.min(length, remaining);
|
|
1240
|
+
remaining -= n;
|
|
1241
|
+
return { bytesRead: n };
|
|
1242
|
+
}),
|
|
1243
|
+
};
|
|
1244
|
+
const got = await readFileRegion(
|
|
1245
|
+
fakeFh as unknown as Parameters<typeof readFileRegion>[0],
|
|
1246
|
+
Buffer.alloc(0),
|
|
1247
|
+
0,
|
|
1248
|
+
100,
|
|
1249
|
+
8,
|
|
1250
|
+
);
|
|
1251
|
+
expect(got).toBe(30);
|
|
1252
|
+
});
|
|
1253
|
+
});
|
package/src/skill-telemetry.ts
CHANGED
|
@@ -594,6 +594,51 @@ async function listJsonlFiles(root: string): Promise<string[]> {
|
|
|
594
594
|
// session context without slurping a multi-MiB transcript.
|
|
595
595
|
const CODEX_META_PREFIX_BYTES = 64 * 1024;
|
|
596
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Largest length passed to a single `FileHandle.read()`. Node's native fs.read
|
|
599
|
+
* asserts `args[3]->IsInt32()` on the length argument, so a single read whose
|
|
600
|
+
* length exceeds 2**31-1 (≈2 GiB) does NOT throw a catchable error — it aborts
|
|
601
|
+
* the whole process with SIGABRT. A skill/session log whose unread tail had
|
|
602
|
+
* grown past ~2 GiB crashed the entire `hq sync` runner this way
|
|
603
|
+
* (Sentry HQ-SYNC-WEB-15). 256 MiB stays comfortably within Int32 while keeping
|
|
604
|
+
* the read-loop iteration count small.
|
|
605
|
+
*/
|
|
606
|
+
const MAX_FS_READ_CHUNK_BYTES = 256 * 1024 * 1024;
|
|
607
|
+
|
|
608
|
+
/** Minimal structural view of the `FileHandle.read` we depend on. */
|
|
609
|
+
type ReadableFileHandle = {
|
|
610
|
+
read(
|
|
611
|
+
buffer: Buffer,
|
|
612
|
+
offset: number,
|
|
613
|
+
length: number,
|
|
614
|
+
position: number,
|
|
615
|
+
): Promise<{ bytesRead: number }>;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Read `length` bytes from `fh` starting at byte `position` into `buf`, issuing
|
|
620
|
+
* native reads no larger than `chunkBytes` so the length argument never exceeds
|
|
621
|
+
* Int32 and trips the SIGABRT-on-assert path (HQ-SYNC-WEB-15). Returns the total
|
|
622
|
+
* bytes actually read — which may be < `length` if the file was truncated
|
|
623
|
+
* between `stat` and this read (a 0-byte read means EOF, so we stop).
|
|
624
|
+
*/
|
|
625
|
+
export async function readFileRegion(
|
|
626
|
+
fh: ReadableFileHandle,
|
|
627
|
+
buf: Buffer,
|
|
628
|
+
position: number,
|
|
629
|
+
length: number,
|
|
630
|
+
chunkBytes: number = MAX_FS_READ_CHUNK_BYTES,
|
|
631
|
+
): Promise<number> {
|
|
632
|
+
let filled = 0;
|
|
633
|
+
while (filled < length) {
|
|
634
|
+
const chunk = Math.min(chunkBytes, length - filled);
|
|
635
|
+
const { bytesRead } = await fh.read(buf, filled, chunk, position + filled);
|
|
636
|
+
if (bytesRead === 0) break; // EOF / truncated since stat — stop cleanly.
|
|
637
|
+
filled += bytesRead;
|
|
638
|
+
}
|
|
639
|
+
return filled;
|
|
640
|
+
}
|
|
641
|
+
|
|
597
642
|
/** Read cwd + sessionId from a Codex rollout's leading `session_meta` line.
|
|
598
643
|
* Always reads from offset 0 (independent of the byte cursor). Best-effort:
|
|
599
644
|
* any error → empty context (events then fail the hqRoot scope filter, which
|
|
@@ -605,7 +650,7 @@ async function readCodexSessionContext(
|
|
|
605
650
|
const fh = await fs.open(filePath, "r");
|
|
606
651
|
try {
|
|
607
652
|
const buf = Buffer.alloc(CODEX_META_PREFIX_BYTES);
|
|
608
|
-
const
|
|
653
|
+
const bytesRead = await readFileRegion(fh, buf, 0, CODEX_META_PREFIX_BYTES);
|
|
609
654
|
const firstLine = buf.toString("utf-8", 0, bytesRead).split("\n", 1)[0]?.trim();
|
|
610
655
|
if (!firstLine) return {};
|
|
611
656
|
return parseCodexSessionMeta(JSON.parse(firstLine)) ?? {};
|
|
@@ -791,8 +836,11 @@ export async function collectAndSendSkillTelemetry(
|
|
|
791
836
|
try {
|
|
792
837
|
const length = Math.max(0, currentSize - offset);
|
|
793
838
|
const buf = Buffer.alloc(length);
|
|
794
|
-
|
|
795
|
-
|
|
839
|
+
// Chunked: a single fh.read() with length > Int32 aborts the process
|
|
840
|
+
// with SIGABRT (HQ-SYNC-WEB-15). `bytesRead` may be < length if the
|
|
841
|
+
// file was truncated since stat — bound the decode to it.
|
|
842
|
+
const bytesRead = await readFileRegion(fh, buf, offset, length);
|
|
843
|
+
content = buf.toString("utf-8", 0, bytesRead);
|
|
796
844
|
} finally {
|
|
797
845
|
await fh.close();
|
|
798
846
|
}
|
package/src/vault-client.ts
CHANGED
|
@@ -114,6 +114,30 @@ export interface EntityInfo {
|
|
|
114
114
|
createdAt: string;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Pick the caller's OWN entity for the `--personal` slot when the runner is an
|
|
119
|
+
* agent machine identity (username `machine-agt_*`, idToken
|
|
120
|
+
* `custom:entityType=agent` / `custom:entityUid=agt_*`).
|
|
121
|
+
*
|
|
122
|
+
* The person-only `pickCanonicalPersonEntity` filters `type === "person"`, so
|
|
123
|
+
* an agent's own entity (`type: "agent"`) is dropped and `--personal` for an
|
|
124
|
+
* agent emits `setup-needed` — never reaching the STS vend / S3 read (the
|
|
125
|
+
* US-004 hard-gate finding). This selector resolves the agent's own entity by
|
|
126
|
+
* matching `selfUid` (the `custom:entityUid` claim) against the self-listing
|
|
127
|
+
* the agent sees, so the personal slot resolves to `hq-vault-agt-<uid>`.
|
|
128
|
+
*
|
|
129
|
+
* Returns null when no entity matches the claimed self-uid (the caller then
|
|
130
|
+
* falls back to the person-only pick / `setup-needed`, exactly as before).
|
|
131
|
+
*/
|
|
132
|
+
export function pickAgentSelfEntity(
|
|
133
|
+
list: EntityInfo[],
|
|
134
|
+
selfUid: string,
|
|
135
|
+
): EntityInfo | null {
|
|
136
|
+
if (!selfUid) return null;
|
|
137
|
+
const own = list.find((e) => e.uid === selfUid && e.type === "agent");
|
|
138
|
+
return own ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
117
141
|
export function pickCanonicalPersonEntity(
|
|
118
142
|
list: EntityInfo[],
|
|
119
143
|
): EntityInfo | null {
|