@wrongstack/governance 0.298.3 → 0.300.0

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.
@@ -112,6 +112,14 @@ export declare class GovernanceCapabilityGrantRegistry {
112
112
  sweepExpired(reason?: string): number;
113
113
  getGrant(grantId: string): GovernanceCapabilityGrant | null;
114
114
  remainingTtlMs(grantId: string): number | null;
115
+ /**
116
+ * Ordering is deliberately byte-wise rather than locale-aware. Both keys are
117
+ * ASCII by construction — `issuedAt` is an ISO-8601 timestamp and `grantId`
118
+ * matches `^[A-Za-z0-9_-]{1,128}$` — so the two orders are identical, but
119
+ * `localeCompare` would make the order depend on the host's default locale.
120
+ * This list backs the `list_capability_grants` cursor, and a cursor is only
121
+ * sound while the order it walks is stable across the requests that walk it.
122
+ */
115
123
  listGrants(): readonly GovernanceCapabilityGrant[];
116
124
  private parseToken;
117
125
  private describe;
@@ -4,6 +4,20 @@ import type { VerificationRunContract } from './verification-run-contract.js';
4
4
  import type { RecordWorkspaceSnapshotResult, WorkspaceSnapshotFenceDescriptor } from './workspace-snapshot-fence.js';
5
5
  export declare const GOVERNANCE_OBSERVATION_CATEGORIES: readonly ['task_created', 'status_changed', 'plan_updated', 'tool_invoked', 'evidence_candidate', 'verification_reported', 'review_reported', 'completion_claimed', 'failure_reported', 'capability_grant_issued', 'capability_grant_revoked', 'capability_grant_expired', 'capability_grant_rotated', 'daemon_attachment_broker_lifecycle', 'daemon_shutdown_requested'];
6
6
  export type GovernanceObservationCategory = (typeof GOVERNANCE_OBSERVATION_CATEGORIES)[number];
7
+ export declare const GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE = 50;
8
+ export declare const GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE = 100;
9
+ export interface ReadGovernanceObservationsPageOptions {
10
+ readonly projectId: string;
11
+ readonly taskId?: string | undefined;
12
+ /**
13
+ * Exact set of categories to return. Callers pass the closed enum subset they
14
+ * are entitled to rather than filtering after the fact, so the database never
15
+ * hands back rows that are about to be discarded.
16
+ */
17
+ readonly categories: readonly GovernanceObservationCategory[];
18
+ readonly afterSequence: number;
19
+ readonly limit: number;
20
+ }
7
21
  export interface GovernanceObservation {
8
22
  readonly observationId: string;
9
23
  readonly projectId: string;
@@ -51,7 +65,7 @@ export interface GovernanceEventStore {
51
65
  readEvents(taskId: string): readonly GovernanceEvent[];
52
66
  readReceipt(commandId: string): GovernanceCommandReceipt | null;
53
67
  appendObservation(observation: GovernanceObservation): AppendGovernanceObservationResult;
54
- readObservations(projectId: string, taskId?: string): readonly StoredGovernanceObservation[];
68
+ readObservationsPage(options: ReadGovernanceObservationsPageOptions): readonly StoredGovernanceObservation[];
55
69
  readEvidenceCandidateObservations(projectId: string, taskId: string, options: {
56
70
  readonly afterSequence: number;
57
71
  readonly limit: number;
@@ -84,7 +98,19 @@ export declare class SqliteGovernanceEventStore implements GovernanceEventStore
84
98
  readEvents(taskId: string): readonly GovernanceEvent[];
85
99
  readReceipt(commandId: string): GovernanceCommandReceipt | null;
86
100
  appendObservation(observation: GovernanceObservation): AppendGovernanceObservationResult;
87
- readObservations(projectId: string, taskId?: string): readonly StoredGovernanceObservation[];
101
+ /**
102
+ * Paginated, category-filtered observation read.
103
+ *
104
+ * The observations table is append-only — its UPDATE and DELETE triggers say
105
+ * so — and therefore grows without bound for the lifetime of a project. The
106
+ * previous unpaginated read loaded every row a project had ever recorded and
107
+ * left the caller to filter in JavaScript, which cost 148 MB and 400 ms at
108
+ * 100 000 rows and produced a 8.9 MB response: already past the 8 MB IPC
109
+ * frame cap, so the endpoint could not succeed at all past that point. Both
110
+ * the category filter and the page window belong in SQL, matching what
111
+ * `readEvidenceCandidateObservations` already does.
112
+ */
113
+ readObservationsPage(options: ReadGovernanceObservationsPageOptions): readonly StoredGovernanceObservation[];
88
114
  readEvidenceCandidateObservations(projectId: string, taskId: string, options: {
89
115
  readonly afterSequence: number;
90
116
  readonly limit: number;
package/dist/index.js CHANGED
@@ -309,12 +309,22 @@ var GovernanceCapabilityGrantRegistry = class {
309
309
  const remaining = record.expiresAtMs - this.now();
310
310
  return remaining > 0 ? remaining : null;
311
311
  }
312
+ /**
313
+ * Ordering is deliberately byte-wise rather than locale-aware. Both keys are
314
+ * ASCII by construction — `issuedAt` is an ISO-8601 timestamp and `grantId`
315
+ * matches `^[A-Za-z0-9_-]{1,128}$` — so the two orders are identical, but
316
+ * `localeCompare` would make the order depend on the host's default locale.
317
+ * This list backs the `list_capability_grants` cursor, and a cursor is only
318
+ * sound while the order it walks is stable across the requests that walk it.
319
+ */
312
320
  listGrants() {
313
321
  const now = this.now();
314
322
  return Object.freeze(
315
- [...this.records.values()].map((record) => this.describe(record, now)).sort(
316
- (left, right) => left.issuedAt.localeCompare(right.issuedAt) || left.grantId.localeCompare(right.grantId)
317
- )
323
+ [...this.records.values()].map((record) => this.describe(record, now)).sort((left, right) => {
324
+ if (left.issuedAt !== right.issuedAt) return left.issuedAt < right.issuedAt ? -1 : 1;
325
+ if (left.grantId === right.grantId) return 0;
326
+ return left.grantId < right.grantId ? -1 : 1;
327
+ })
318
328
  );
319
329
  }
320
330
  parseToken(token) {
@@ -1076,7 +1086,8 @@ var GovernanceProjectClient = class {
1076
1086
  return new Promise((resolve6, reject3) => {
1077
1087
  const socket = net2.createConnection(this.endpoint);
1078
1088
  let settled = false;
1079
- let buffer = Buffer.alloc(0);
1089
+ const chunks = [];
1090
+ let size = 0;
1080
1091
  const finish = (error, response) => {
1081
1092
  if (settled) return;
1082
1093
  settled = true;
@@ -1090,15 +1101,21 @@ var GovernanceProjectClient = class {
1090
1101
  }, this.timeoutMs);
1091
1102
  socket.once("connect", () => socket.write(frame));
1092
1103
  socket.on("data", (chunk) => {
1093
- buffer = Buffer.concat([buffer, chunk]);
1094
- if (buffer.byteLength > GOVERNANCE_IPC_MAX_FRAME_BYTES) {
1104
+ if (settled) return;
1105
+ const newline = chunk.indexOf(10);
1106
+ const frameBytes = size + (newline < 0 ? chunk.byteLength : newline);
1107
+ if (frameBytes > GOVERNANCE_IPC_MAX_FRAME_BYTES) {
1095
1108
  finish(new Error("Governance IPC response exceeded the frame limit."));
1096
1109
  return;
1097
1110
  }
1098
- const newline = buffer.indexOf(10);
1099
- if (newline < 0) return;
1111
+ if (newline < 0) {
1112
+ chunks.push(chunk);
1113
+ size = frameBytes;
1114
+ return;
1115
+ }
1116
+ chunks.push(chunk.subarray(0, newline));
1100
1117
  try {
1101
- const parsed = JSON.parse(buffer.subarray(0, newline).toString("utf8"));
1118
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
1102
1119
  const response = decodeGovernanceIpcResponse(parsed);
1103
1120
  const expectedRequestId = request && typeof request === "object" && "requestId" in request && typeof request.requestId === "string" ? request.requestId : null;
1104
1121
  if (expectedRequestId && response.requestId !== expectedRequestId) {
@@ -1204,7 +1221,9 @@ var GOVERNANCE_CREDENTIAL_LEASE_DEFAULT_RETRY_MS = 1e3;
1204
1221
  var GOVERNANCE_CREDENTIAL_LEASE_DEFAULT_MAX_ATTEMPTS = 3;
1205
1222
  var defaultScheduler = {
1206
1223
  schedule(callback, delayMs) {
1207
- return setTimeout(callback, delayMs);
1224
+ const handle = setTimeout(callback, delayMs);
1225
+ handle.unref();
1226
+ return handle;
1208
1227
  },
1209
1228
  cancel(handle) {
1210
1229
  clearTimeout(handle);
@@ -1438,7 +1457,7 @@ var GovernanceCredentialLeaseController = class {
1438
1457
  this.timer = this.scheduler.schedule(() => {
1439
1458
  this.timer = void 0;
1440
1459
  this.nextActionAtMs = void 0;
1441
- void this.rotateNow();
1460
+ void this.rotateNow().catch(() => void 0);
1442
1461
  }, delayMs);
1443
1462
  }
1444
1463
  clearTimer() {
@@ -2245,27 +2264,29 @@ function findCycle(steps, edges) {
2245
2264
  for (const edge of edges) adjacency.get(edge.fromStepId)?.push(edge.toStepId);
2246
2265
  const visiting = /* @__PURE__ */ new Set();
2247
2266
  const visited = /* @__PURE__ */ new Set();
2248
- const stack = [];
2249
- const visit = (stepId) => {
2250
- if (visiting.has(stepId)) {
2251
- const cycleStart = stack.indexOf(stepId);
2252
- return [...stack.slice(cycleStart), stepId];
2253
- }
2254
- if (visited.has(stepId)) return null;
2255
- visiting.add(stepId);
2256
- stack.push(stepId);
2257
- for (const next of adjacency.get(stepId) ?? []) {
2258
- const cycle = visit(next);
2259
- if (cycle) return cycle;
2260
- }
2261
- stack.pop();
2262
- visiting.delete(stepId);
2263
- visited.add(stepId);
2264
- return null;
2265
- };
2267
+ const path6 = [];
2266
2268
  for (const step of steps) {
2267
- const cycle = visit(step.id);
2268
- if (cycle) return cycle;
2269
+ if (visited.has(step.id)) continue;
2270
+ visiting.add(step.id);
2271
+ path6.push(step.id);
2272
+ const frames = [{ id: step.id, next: adjacency.get(step.id) ?? [], index: 0 }];
2273
+ while (frames.length > 0) {
2274
+ const frame = frames[frames.length - 1];
2275
+ if (frame.index < frame.next.length) {
2276
+ const next = frame.next[frame.index];
2277
+ frame.index += 1;
2278
+ if (visiting.has(next)) return [...path6.slice(path6.indexOf(next)), next];
2279
+ if (visited.has(next)) continue;
2280
+ visiting.add(next);
2281
+ path6.push(next);
2282
+ frames.push({ id: next, next: adjacency.get(next) ?? [], index: 0 });
2283
+ continue;
2284
+ }
2285
+ frames.pop();
2286
+ path6.pop();
2287
+ visiting.delete(frame.id);
2288
+ visited.add(frame.id);
2289
+ }
2269
2290
  }
2270
2291
  return null;
2271
2292
  }
@@ -4393,6 +4414,8 @@ var GOVERNANCE_OBSERVATION_CATEGORIES = [
4393
4414
  "daemon_attachment_broker_lifecycle",
4394
4415
  "daemon_shutdown_requested"
4395
4416
  ];
4417
+ var GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE = 50;
4418
+ var GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE = 100;
4396
4419
  var GovernanceStoreCorruptionError = class extends Error {
4397
4420
  constructor(message) {
4398
4421
  super(message);
@@ -4590,21 +4613,47 @@ var SqliteGovernanceEventStore = class _SqliteGovernanceEventStore {
4590
4613
  throw error;
4591
4614
  }
4592
4615
  }
4593
- readObservations(projectId, taskId) {
4616
+ /**
4617
+ * Paginated, category-filtered observation read.
4618
+ *
4619
+ * The observations table is append-only — its UPDATE and DELETE triggers say
4620
+ * so — and therefore grows without bound for the lifetime of a project. The
4621
+ * previous unpaginated read loaded every row a project had ever recorded and
4622
+ * left the caller to filter in JavaScript, which cost 148 MB and 400 ms at
4623
+ * 100 000 rows and produced a 8.9 MB response: already past the 8 MB IPC
4624
+ * frame cap, so the endpoint could not succeed at all past that point. Both
4625
+ * the category filter and the page window belong in SQL, matching what
4626
+ * `readEvidenceCandidateObservations` already does.
4627
+ */
4628
+ readObservationsPage(options) {
4594
4629
  this.assertOpen();
4595
- const rows = taskId === void 0 ? this.db.prepare(
4596
- `SELECT sequence, observation_id, project_id, task_id, source, category,
4597
- observation_json, observation_hash, observed_at, recorded_at
4598
- FROM governance_observations
4599
- WHERE project_id = ?
4600
- ORDER BY sequence`
4601
- ).all(projectId) : this.db.prepare(
4630
+ if (!Number.isSafeInteger(options.afterSequence) || options.afterSequence < 0) {
4631
+ throw new Error("Observation cursor must be a non-negative safe integer.");
4632
+ }
4633
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE + 1) {
4634
+ throw new Error(
4635
+ `Observation query limit must be between 1 and ${GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE + 1}.`
4636
+ );
4637
+ }
4638
+ if (options.categories.length === 0) return Object.freeze([]);
4639
+ const parameters = [options.projectId];
4640
+ let where = "project_id = ?";
4641
+ if (options.taskId !== void 0) {
4642
+ where += " AND task_id = ?";
4643
+ parameters.push(options.taskId);
4644
+ }
4645
+ where += ` AND category IN (${options.categories.map(() => "?").join(",")})`;
4646
+ parameters.push(...options.categories);
4647
+ where += " AND sequence > ?";
4648
+ parameters.push(options.afterSequence, options.limit);
4649
+ const rows = this.db.prepare(
4602
4650
  `SELECT sequence, observation_id, project_id, task_id, source, category,
4603
- observation_json, observation_hash, observed_at, recorded_at
4604
- FROM governance_observations
4605
- WHERE project_id = ? AND task_id = ?
4606
- ORDER BY sequence`
4607
- ).all(projectId, taskId);
4651
+ observation_json, observation_hash, observed_at, recorded_at
4652
+ FROM governance_observations
4653
+ WHERE ${where}
4654
+ ORDER BY sequence
4655
+ LIMIT ?`
4656
+ ).all(...parameters);
4608
4657
  return Object.freeze(rows.map((row) => this.observationFromRow(row)));
4609
4658
  }
4610
4659
  readEvidenceCandidateObservations(projectId, taskId, options) {
@@ -4878,7 +4927,18 @@ var MAX_NESTING_DEPTH = 32;
4878
4927
  var MAX_ARRAY_ITEMS = 1e4;
4879
4928
  var MAX_OBJECT_KEYS = 1e3;
4880
4929
  var MAX_STRING_LENGTH = 1e6;
4930
+ var MAX_ISSUES = 100;
4881
4931
  function issue2(issues, code, path6, message) {
4932
+ if (issues.length >= MAX_ISSUES) {
4933
+ if (issues.length === MAX_ISSUES) {
4934
+ issues.push({
4935
+ code: "resource_limit",
4936
+ path: "$",
4937
+ message: `Validation stopped after ${MAX_ISSUES} issues.`
4938
+ });
4939
+ }
4940
+ return;
4941
+ }
4882
4942
  issues.push({ code, path: path6, message });
4883
4943
  }
4884
4944
  function plainRecord4(value, path6, issues) {
@@ -4970,23 +5030,24 @@ function enumArray(value, allowed, path6, issues) {
4970
5030
  (entry, entryPath, target) => enumValue(entry, allowed, entryPath, target)
4971
5031
  );
4972
5032
  }
4973
- function capabilityArray(value, path6, issues) {
5033
+ function boundedCapabilityArray(value, allowed, noun, path6, issues) {
4974
5034
  if (!Array.isArray(value)) {
4975
5035
  issue2(issues, "expected_array", path6, `${path6} must be an array.`);
4976
5036
  return;
4977
5037
  }
4978
- if (value.length === 0 || value.length > GOVERNANCE_SERVICE_CAPABILITIES.length) {
5038
+ if (value.length === 0 || value.length > allowed.length) {
4979
5039
  issue2(
4980
5040
  issues,
4981
5041
  "resource_limit",
4982
5042
  path6,
4983
- `${path6} must contain between 1 and ${GOVERNANCE_SERVICE_CAPABILITIES.length} capabilities.`
5043
+ `${path6} must contain between 1 and ${allowed.length} ${noun}.`
4984
5044
  );
5045
+ return;
4985
5046
  }
4986
5047
  const seen = /* @__PURE__ */ new Set();
4987
5048
  for (let index = 0; index < value.length; index += 1) {
4988
5049
  const entryPath = `${path6}[${index}]`;
4989
- enumValue(value[index], GOVERNANCE_SERVICE_CAPABILITIES, entryPath, issues);
5050
+ enumValue(value[index], allowed, entryPath, issues);
4990
5051
  if (typeof value[index] === "string") {
4991
5052
  if (seen.has(value[index])) {
4992
5053
  issue2(issues, "semantic_invalid", entryPath, `${entryPath} duplicates a capability.`);
@@ -4995,30 +5056,17 @@ function capabilityArray(value, path6, issues) {
4995
5056
  }
4996
5057
  }
4997
5058
  }
5059
+ function capabilityArray(value, path6, issues) {
5060
+ boundedCapabilityArray(value, GOVERNANCE_SERVICE_CAPABILITIES, "capabilities", path6, issues);
5061
+ }
4998
5062
  function runtimeModelCapabilityArray(value, path6, issues) {
4999
- if (!Array.isArray(value)) {
5000
- issue2(issues, "expected_array", path6, `${path6} must be an array.`);
5001
- return;
5002
- }
5003
- if (value.length === 0 || value.length > GOVERNANCE_RUNTIME_MODEL_CAPABILITIES.length) {
5004
- issue2(
5005
- issues,
5006
- "resource_limit",
5007
- path6,
5008
- `${path6} must contain between 1 and ${GOVERNANCE_RUNTIME_MODEL_CAPABILITIES.length} model-safe capabilities.`
5009
- );
5010
- }
5011
- const seen = /* @__PURE__ */ new Set();
5012
- for (let index = 0; index < value.length; index += 1) {
5013
- const entryPath = `${path6}[${index}]`;
5014
- enumValue(value[index], GOVERNANCE_RUNTIME_MODEL_CAPABILITIES, entryPath, issues);
5015
- if (typeof value[index] === "string") {
5016
- if (seen.has(value[index])) {
5017
- issue2(issues, "semantic_invalid", entryPath, `${entryPath} duplicates a capability.`);
5018
- }
5019
- seen.add(value[index]);
5020
- }
5021
- }
5063
+ boundedCapabilityArray(
5064
+ value,
5065
+ GOVERNANCE_RUNTIME_MODEL_CAPABILITIES,
5066
+ "model-safe capabilities",
5067
+ path6,
5068
+ issues
5069
+ );
5022
5070
  }
5023
5071
  function jsonValue(value, path6, issues, depth = 0) {
5024
5072
  if (depth > MAX_NESTING_DEPTH) {
@@ -5328,6 +5376,21 @@ function validateObservation(value, path6, issues) {
5328
5376
  const payload = plainRecord4(record.payload, `${path6}.payload`, issues);
5329
5377
  if (payload) jsonValue(payload, `${path6}.payload`, issues);
5330
5378
  }
5379
+ function observationPageWindow(record, issues) {
5380
+ if (record.afterSequence !== void 0) {
5381
+ integerValue(record.afterSequence, "$.afterSequence", issues);
5382
+ }
5383
+ if (record.limit === void 0) return;
5384
+ integerValue(record.limit, "$.limit", issues, 1);
5385
+ if (typeof record.limit === "number" && record.limit > GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE) {
5386
+ issue2(
5387
+ issues,
5388
+ "resource_limit",
5389
+ "$.limit",
5390
+ `$.limit must not exceed ${GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE}.`
5391
+ );
5392
+ }
5393
+ }
5331
5394
  function deepFreeze4(value) {
5332
5395
  if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
5333
5396
  for (const nested of Object.values(value)) deepFreeze4(nested);
@@ -5360,8 +5423,14 @@ function decodeGovernanceServiceRequest(input) {
5360
5423
  stringValue(record.commandId, "$.commandId", issues);
5361
5424
  break;
5362
5425
  case "read_observations":
5363
- exactKeys2(record, ["protocolVersion", "requestId", "type", "taskId"], "$", issues);
5426
+ exactKeys2(
5427
+ record,
5428
+ ["protocolVersion", "requestId", "type", "taskId", "afterSequence", "limit"],
5429
+ "$",
5430
+ issues
5431
+ );
5364
5432
  if (record.taskId !== void 0) stringValue(record.taskId, "$.taskId", issues);
5433
+ observationPageWindow(record, issues);
5365
5434
  break;
5366
5435
  case "read_evidence_candidates":
5367
5436
  exactKeys2(
@@ -5387,6 +5456,14 @@ function decodeGovernanceServiceRequest(input) {
5387
5456
  }
5388
5457
  break;
5389
5458
  case "read_audit_observations":
5459
+ exactKeys2(
5460
+ record,
5461
+ ["protocolVersion", "requestId", "type", "afterSequence", "limit"],
5462
+ "$",
5463
+ issues
5464
+ );
5465
+ observationPageWindow(record, issues);
5466
+ break;
5390
5467
  case "read_own_capability_grant":
5391
5468
  case "read_daemon_status":
5392
5469
  exactKeys2(record, ["protocolVersion", "requestId", "type"], "$", issues);
@@ -6577,6 +6654,12 @@ function fail(requestId, code, message, details) {
6577
6654
  function reservedAuditCategory(category) {
6578
6655
  return category.startsWith("capability_grant_") || category.startsWith("daemon_");
6579
6656
  }
6657
+ var AUDIT_OBSERVATION_CATEGORIES = Object.freeze(
6658
+ GOVERNANCE_OBSERVATION_CATEGORIES.filter((category) => reservedAuditCategory(category))
6659
+ );
6660
+ var TASK_OBSERVATION_CATEGORIES = Object.freeze(
6661
+ GOVERNANCE_OBSERVATION_CATEGORIES.filter((category) => !reservedAuditCategory(category))
6662
+ );
6580
6663
  var GovernanceProjectService = class {
6581
6664
  constructor(projectId, store, resolveDecisionContext = () => ({})) {
6582
6665
  this.projectId = projectId;
@@ -6704,14 +6787,13 @@ var GovernanceProjectService = class {
6704
6787
  };
6705
6788
  }
6706
6789
  case "read_observations":
6707
- return {
6708
- ok: true,
6790
+ return this.observationPage(
6709
6791
  requestId,
6710
- result: {
6711
- type: "observations",
6712
- observations: this.store.readObservations(this.projectId, request.taskId).filter((observation) => !reservedAuditCategory(observation.category))
6713
- }
6714
- };
6792
+ TASK_OBSERVATION_CATEGORIES,
6793
+ request.taskId,
6794
+ request.afterSequence,
6795
+ request.limit
6796
+ );
6715
6797
  case "read_evidence_candidates": {
6716
6798
  const limit = request.limit ?? GOVERNANCE_EVIDENCE_CANDIDATE_DEFAULT_PAGE_SIZE;
6717
6799
  const rows = this.store.readEvidenceCandidateObservations(this.projectId, request.taskId, {
@@ -6735,14 +6817,13 @@ var GovernanceProjectService = class {
6735
6817
  };
6736
6818
  }
6737
6819
  case "read_audit_observations":
6738
- return {
6739
- ok: true,
6820
+ return this.observationPage(
6740
6821
  requestId,
6741
- result: {
6742
- type: "observations",
6743
- observations: this.store.readObservations(this.projectId).filter((observation) => reservedAuditCategory(observation.category))
6744
- }
6745
- };
6822
+ AUDIT_OBSERVATION_CATEGORIES,
6823
+ void 0,
6824
+ request.afterSequence,
6825
+ request.limit
6826
+ );
6746
6827
  case "submit_command": {
6747
6828
  if (!this.commandBelongsToProject(request.command)) {
6748
6829
  return fail(
@@ -6804,6 +6885,31 @@ var GovernanceProjectService = class {
6804
6885
  );
6805
6886
  }
6806
6887
  }
6888
+ /**
6889
+ * Shared body for both observation reads. Fetches one row past the page so
6890
+ * the presence of a further page is known without a second query — the same
6891
+ * convention `read_evidence_candidates` uses.
6892
+ */
6893
+ observationPage(requestId, categories, taskId, afterSequence, limit) {
6894
+ const pageSize = limit ?? GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE;
6895
+ const rows = this.store.readObservationsPage({
6896
+ projectId: this.projectId,
6897
+ ...taskId === void 0 ? {} : { taskId },
6898
+ categories,
6899
+ afterSequence: afterSequence ?? 0,
6900
+ limit: pageSize + 1
6901
+ });
6902
+ const page = rows.slice(0, pageSize);
6903
+ return {
6904
+ ok: true,
6905
+ requestId,
6906
+ result: {
6907
+ type: "observations",
6908
+ observations: page,
6909
+ nextAfterSequence: rows.length > pageSize && page.length > 0 ? page.at(-1)?.sequence ?? null : null
6910
+ }
6911
+ };
6912
+ }
6807
6913
  commandBelongsToProject(command) {
6808
6914
  if (command.type === "create_task") {
6809
6915
  return command.projectId === this.projectId && command.contract.projectId === this.projectId;
@@ -7078,12 +7184,14 @@ var GovernanceProjectServer = class {
7078
7184
  else socket.destroy();
7079
7185
  }
7080
7186
  accept(socket) {
7081
- let buffer = Buffer.alloc(0);
7187
+ const chunks = [];
7188
+ let size = 0;
7082
7189
  let handled = false;
7083
7190
  socket.on("data", (chunk) => {
7084
7191
  if (handled) return;
7085
- buffer = Buffer.concat([buffer, chunk]);
7086
- if (buffer.byteLength > GOVERNANCE_IPC_MAX_FRAME_BYTES) {
7192
+ const newline = chunk.indexOf(10);
7193
+ const frameBytes = size + (newline < 0 ? chunk.byteLength : newline);
7194
+ if (frameBytes > GOVERNANCE_IPC_MAX_FRAME_BYTES) {
7087
7195
  handled = true;
7088
7196
  this.send(
7089
7197
  socket,
@@ -7091,10 +7199,14 @@ var GovernanceProjectServer = class {
7091
7199
  );
7092
7200
  return;
7093
7201
  }
7094
- const newline = buffer.indexOf(10);
7095
- if (newline < 0) return;
7202
+ if (newline < 0) {
7203
+ chunks.push(chunk);
7204
+ size = frameBytes;
7205
+ return;
7206
+ }
7096
7207
  handled = true;
7097
- const line = buffer.subarray(0, newline).toString("utf8").replace(/\r$/, "");
7208
+ chunks.push(chunk.subarray(0, newline));
7209
+ const line = Buffer.concat(chunks).toString("utf8").replace(/\r$/, "");
7098
7210
  this.handleLine(socket, line);
7099
7211
  });
7100
7212
  }
@@ -7325,10 +7437,10 @@ var GovernanceCompatibilityRuntime = class {
7325
7437
  this.#closePromise = this.revokeModelGrant().then((response) => {
7326
7438
  if (response.ok && response.result.type === "capability_grant_revoked") {
7327
7439
  this.#closed = true;
7328
- adminSession.stop();
7329
7440
  }
7330
7441
  return response;
7331
7442
  }).finally(() => {
7443
+ adminSession.stop();
7332
7444
  this.#closePromise = void 0;
7333
7445
  });
7334
7446
  return this.#closePromise;
@@ -8348,6 +8460,8 @@ export {
8348
8460
  GOVERNANCE_MANAGEMENT_RECEIPT_MAX_ENTRIES,
8349
8461
  GOVERNANCE_MANAGEMENT_RECEIPT_TTL_MS,
8350
8462
  GOVERNANCE_OBSERVATION_CATEGORIES,
8463
+ GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE,
8464
+ GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE,
8351
8465
  GOVERNANCE_RUNTIME_ATTACHMENT_MAX_TTL_MS,
8352
8466
  GOVERNANCE_SERVICE_CAPABILITIES,
8353
8467
  GOVERNANCE_SERVICE_PROTOCOL_VERSION,
@@ -1333,27 +1333,29 @@ function findCycle(steps, edges) {
1333
1333
  for (const edge of edges) adjacency.get(edge.fromStepId)?.push(edge.toStepId);
1334
1334
  const visiting = /* @__PURE__ */ new Set();
1335
1335
  const visited = /* @__PURE__ */ new Set();
1336
- const stack = [];
1337
- const visit = (stepId) => {
1338
- if (visiting.has(stepId)) {
1339
- const cycleStart = stack.indexOf(stepId);
1340
- return [...stack.slice(cycleStart), stepId];
1341
- }
1342
- if (visited.has(stepId)) return null;
1343
- visiting.add(stepId);
1344
- stack.push(stepId);
1345
- for (const next of adjacency.get(stepId) ?? []) {
1346
- const cycle = visit(next);
1347
- if (cycle) return cycle;
1348
- }
1349
- stack.pop();
1350
- visiting.delete(stepId);
1351
- visited.add(stepId);
1352
- return null;
1353
- };
1336
+ const path5 = [];
1354
1337
  for (const step of steps) {
1355
- const cycle = visit(step.id);
1356
- if (cycle) return cycle;
1338
+ if (visited.has(step.id)) continue;
1339
+ visiting.add(step.id);
1340
+ path5.push(step.id);
1341
+ const frames = [{ id: step.id, next: adjacency.get(step.id) ?? [], index: 0 }];
1342
+ while (frames.length > 0) {
1343
+ const frame = frames[frames.length - 1];
1344
+ if (frame.index < frame.next.length) {
1345
+ const next = frame.next[frame.index];
1346
+ frame.index += 1;
1347
+ if (visiting.has(next)) return [...path5.slice(path5.indexOf(next)), next];
1348
+ if (visited.has(next)) continue;
1349
+ visiting.add(next);
1350
+ path5.push(next);
1351
+ frames.push({ id: next, next: adjacency.get(next) ?? [], index: 0 });
1352
+ continue;
1353
+ }
1354
+ frames.pop();
1355
+ path5.pop();
1356
+ visiting.delete(frame.id);
1357
+ visited.add(frame.id);
1358
+ }
1357
1359
  }
1358
1360
  return null;
1359
1361
  }
@@ -2907,6 +2909,8 @@ var GOVERNANCE_OBSERVATION_CATEGORIES = [
2907
2909
  "daemon_attachment_broker_lifecycle",
2908
2910
  "daemon_shutdown_requested"
2909
2911
  ];
2912
+ var GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE = 50;
2913
+ var GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE = 100;
2910
2914
  var GovernanceStoreCorruptionError = class extends Error {
2911
2915
  constructor(message) {
2912
2916
  super(message);
@@ -3104,21 +3108,47 @@ var SqliteGovernanceEventStore = class _SqliteGovernanceEventStore {
3104
3108
  throw error;
3105
3109
  }
3106
3110
  }
3107
- readObservations(projectId, taskId) {
3111
+ /**
3112
+ * Paginated, category-filtered observation read.
3113
+ *
3114
+ * The observations table is append-only — its UPDATE and DELETE triggers say
3115
+ * so — and therefore grows without bound for the lifetime of a project. The
3116
+ * previous unpaginated read loaded every row a project had ever recorded and
3117
+ * left the caller to filter in JavaScript, which cost 148 MB and 400 ms at
3118
+ * 100 000 rows and produced a 8.9 MB response: already past the 8 MB IPC
3119
+ * frame cap, so the endpoint could not succeed at all past that point. Both
3120
+ * the category filter and the page window belong in SQL, matching what
3121
+ * `readEvidenceCandidateObservations` already does.
3122
+ */
3123
+ readObservationsPage(options) {
3108
3124
  this.assertOpen();
3109
- const rows = taskId === void 0 ? this.db.prepare(
3110
- `SELECT sequence, observation_id, project_id, task_id, source, category,
3111
- observation_json, observation_hash, observed_at, recorded_at
3112
- FROM governance_observations
3113
- WHERE project_id = ?
3114
- ORDER BY sequence`
3115
- ).all(projectId) : this.db.prepare(
3125
+ if (!Number.isSafeInteger(options.afterSequence) || options.afterSequence < 0) {
3126
+ throw new Error("Observation cursor must be a non-negative safe integer.");
3127
+ }
3128
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE + 1) {
3129
+ throw new Error(
3130
+ `Observation query limit must be between 1 and ${GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE + 1}.`
3131
+ );
3132
+ }
3133
+ if (options.categories.length === 0) return Object.freeze([]);
3134
+ const parameters = [options.projectId];
3135
+ let where = "project_id = ?";
3136
+ if (options.taskId !== void 0) {
3137
+ where += " AND task_id = ?";
3138
+ parameters.push(options.taskId);
3139
+ }
3140
+ where += ` AND category IN (${options.categories.map(() => "?").join(",")})`;
3141
+ parameters.push(...options.categories);
3142
+ where += " AND sequence > ?";
3143
+ parameters.push(options.afterSequence, options.limit);
3144
+ const rows = this.db.prepare(
3116
3145
  `SELECT sequence, observation_id, project_id, task_id, source, category,
3117
- observation_json, observation_hash, observed_at, recorded_at
3118
- FROM governance_observations
3119
- WHERE project_id = ? AND task_id = ?
3120
- ORDER BY sequence`
3121
- ).all(projectId, taskId);
3146
+ observation_json, observation_hash, observed_at, recorded_at
3147
+ FROM governance_observations
3148
+ WHERE ${where}
3149
+ ORDER BY sequence
3150
+ LIMIT ?`
3151
+ ).all(...parameters);
3122
3152
  return Object.freeze(rows.map((row) => this.observationFromRow(row)));
3123
3153
  }
3124
3154
  readEvidenceCandidateObservations(projectId, taskId, options) {
@@ -3379,7 +3409,18 @@ var MAX_NESTING_DEPTH = 32;
3379
3409
  var MAX_ARRAY_ITEMS = 1e4;
3380
3410
  var MAX_OBJECT_KEYS = 1e3;
3381
3411
  var MAX_STRING_LENGTH = 1e6;
3412
+ var MAX_ISSUES = 100;
3382
3413
  function issue(issues, code, path5, message) {
3414
+ if (issues.length >= MAX_ISSUES) {
3415
+ if (issues.length === MAX_ISSUES) {
3416
+ issues.push({
3417
+ code: "resource_limit",
3418
+ path: "$",
3419
+ message: `Validation stopped after ${MAX_ISSUES} issues.`
3420
+ });
3421
+ }
3422
+ return;
3423
+ }
3383
3424
  issues.push({ code, path: path5, message });
3384
3425
  }
3385
3426
  function plainRecord4(value, path5, issues) {
@@ -3471,23 +3512,24 @@ function enumArray(value, allowed, path5, issues) {
3471
3512
  (entry, entryPath, target) => enumValue(entry, allowed, entryPath, target)
3472
3513
  );
3473
3514
  }
3474
- function capabilityArray(value, path5, issues) {
3515
+ function boundedCapabilityArray(value, allowed, noun, path5, issues) {
3475
3516
  if (!Array.isArray(value)) {
3476
3517
  issue(issues, "expected_array", path5, `${path5} must be an array.`);
3477
3518
  return;
3478
3519
  }
3479
- if (value.length === 0 || value.length > GOVERNANCE_SERVICE_CAPABILITIES.length) {
3520
+ if (value.length === 0 || value.length > allowed.length) {
3480
3521
  issue(
3481
3522
  issues,
3482
3523
  "resource_limit",
3483
3524
  path5,
3484
- `${path5} must contain between 1 and ${GOVERNANCE_SERVICE_CAPABILITIES.length} capabilities.`
3525
+ `${path5} must contain between 1 and ${allowed.length} ${noun}.`
3485
3526
  );
3527
+ return;
3486
3528
  }
3487
3529
  const seen = /* @__PURE__ */ new Set();
3488
3530
  for (let index = 0; index < value.length; index += 1) {
3489
3531
  const entryPath = `${path5}[${index}]`;
3490
- enumValue(value[index], GOVERNANCE_SERVICE_CAPABILITIES, entryPath, issues);
3532
+ enumValue(value[index], allowed, entryPath, issues);
3491
3533
  if (typeof value[index] === "string") {
3492
3534
  if (seen.has(value[index])) {
3493
3535
  issue(issues, "semantic_invalid", entryPath, `${entryPath} duplicates a capability.`);
@@ -3496,30 +3538,17 @@ function capabilityArray(value, path5, issues) {
3496
3538
  }
3497
3539
  }
3498
3540
  }
3541
+ function capabilityArray(value, path5, issues) {
3542
+ boundedCapabilityArray(value, GOVERNANCE_SERVICE_CAPABILITIES, "capabilities", path5, issues);
3543
+ }
3499
3544
  function runtimeModelCapabilityArray(value, path5, issues) {
3500
- if (!Array.isArray(value)) {
3501
- issue(issues, "expected_array", path5, `${path5} must be an array.`);
3502
- return;
3503
- }
3504
- if (value.length === 0 || value.length > GOVERNANCE_RUNTIME_MODEL_CAPABILITIES.length) {
3505
- issue(
3506
- issues,
3507
- "resource_limit",
3508
- path5,
3509
- `${path5} must contain between 1 and ${GOVERNANCE_RUNTIME_MODEL_CAPABILITIES.length} model-safe capabilities.`
3510
- );
3511
- }
3512
- const seen = /* @__PURE__ */ new Set();
3513
- for (let index = 0; index < value.length; index += 1) {
3514
- const entryPath = `${path5}[${index}]`;
3515
- enumValue(value[index], GOVERNANCE_RUNTIME_MODEL_CAPABILITIES, entryPath, issues);
3516
- if (typeof value[index] === "string") {
3517
- if (seen.has(value[index])) {
3518
- issue(issues, "semantic_invalid", entryPath, `${entryPath} duplicates a capability.`);
3519
- }
3520
- seen.add(value[index]);
3521
- }
3522
- }
3545
+ boundedCapabilityArray(
3546
+ value,
3547
+ GOVERNANCE_RUNTIME_MODEL_CAPABILITIES,
3548
+ "model-safe capabilities",
3549
+ path5,
3550
+ issues
3551
+ );
3523
3552
  }
3524
3553
  function jsonValue(value, path5, issues, depth = 0) {
3525
3554
  if (depth > MAX_NESTING_DEPTH) {
@@ -3829,6 +3858,21 @@ function validateObservation(value, path5, issues) {
3829
3858
  const payload = plainRecord4(record.payload, `${path5}.payload`, issues);
3830
3859
  if (payload) jsonValue(payload, `${path5}.payload`, issues);
3831
3860
  }
3861
+ function observationPageWindow(record, issues) {
3862
+ if (record.afterSequence !== void 0) {
3863
+ integerValue(record.afterSequence, "$.afterSequence", issues);
3864
+ }
3865
+ if (record.limit === void 0) return;
3866
+ integerValue(record.limit, "$.limit", issues, 1);
3867
+ if (typeof record.limit === "number" && record.limit > GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE) {
3868
+ issue(
3869
+ issues,
3870
+ "resource_limit",
3871
+ "$.limit",
3872
+ `$.limit must not exceed ${GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE}.`
3873
+ );
3874
+ }
3875
+ }
3832
3876
  function deepFreeze4(value) {
3833
3877
  if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
3834
3878
  for (const nested of Object.values(value)) deepFreeze4(nested);
@@ -3861,8 +3905,14 @@ function decodeGovernanceServiceRequest(input) {
3861
3905
  stringValue(record.commandId, "$.commandId", issues);
3862
3906
  break;
3863
3907
  case "read_observations":
3864
- exactKeys2(record, ["protocolVersion", "requestId", "type", "taskId"], "$", issues);
3908
+ exactKeys2(
3909
+ record,
3910
+ ["protocolVersion", "requestId", "type", "taskId", "afterSequence", "limit"],
3911
+ "$",
3912
+ issues
3913
+ );
3865
3914
  if (record.taskId !== void 0) stringValue(record.taskId, "$.taskId", issues);
3915
+ observationPageWindow(record, issues);
3866
3916
  break;
3867
3917
  case "read_evidence_candidates":
3868
3918
  exactKeys2(
@@ -3888,6 +3938,14 @@ function decodeGovernanceServiceRequest(input) {
3888
3938
  }
3889
3939
  break;
3890
3940
  case "read_audit_observations":
3941
+ exactKeys2(
3942
+ record,
3943
+ ["protocolVersion", "requestId", "type", "afterSequence", "limit"],
3944
+ "$",
3945
+ issues
3946
+ );
3947
+ observationPageWindow(record, issues);
3948
+ break;
3891
3949
  case "read_own_capability_grant":
3892
3950
  case "read_daemon_status":
3893
3951
  exactKeys2(record, ["protocolVersion", "requestId", "type"], "$", issues);
@@ -4722,12 +4780,22 @@ var GovernanceCapabilityGrantRegistry = class {
4722
4780
  const remaining = record.expiresAtMs - this.now();
4723
4781
  return remaining > 0 ? remaining : null;
4724
4782
  }
4783
+ /**
4784
+ * Ordering is deliberately byte-wise rather than locale-aware. Both keys are
4785
+ * ASCII by construction — `issuedAt` is an ISO-8601 timestamp and `grantId`
4786
+ * matches `^[A-Za-z0-9_-]{1,128}$` — so the two orders are identical, but
4787
+ * `localeCompare` would make the order depend on the host's default locale.
4788
+ * This list backs the `list_capability_grants` cursor, and a cursor is only
4789
+ * sound while the order it walks is stable across the requests that walk it.
4790
+ */
4725
4791
  listGrants() {
4726
4792
  const now = this.now();
4727
4793
  return Object.freeze(
4728
- [...this.records.values()].map((record) => this.describe(record, now)).sort(
4729
- (left, right) => left.issuedAt.localeCompare(right.issuedAt) || left.grantId.localeCompare(right.grantId)
4730
- )
4794
+ [...this.records.values()].map((record) => this.describe(record, now)).sort((left, right) => {
4795
+ if (left.issuedAt !== right.issuedAt) return left.issuedAt < right.issuedAt ? -1 : 1;
4796
+ if (left.grantId === right.grantId) return 0;
4797
+ return left.grantId < right.grantId ? -1 : 1;
4798
+ })
4731
4799
  );
4732
4800
  }
4733
4801
  parseToken(token) {
@@ -4871,6 +4939,12 @@ function fail(requestId, code, message, details) {
4871
4939
  function reservedAuditCategory(category) {
4872
4940
  return category.startsWith("capability_grant_") || category.startsWith("daemon_");
4873
4941
  }
4942
+ var AUDIT_OBSERVATION_CATEGORIES = Object.freeze(
4943
+ GOVERNANCE_OBSERVATION_CATEGORIES.filter((category) => reservedAuditCategory(category))
4944
+ );
4945
+ var TASK_OBSERVATION_CATEGORIES = Object.freeze(
4946
+ GOVERNANCE_OBSERVATION_CATEGORIES.filter((category) => !reservedAuditCategory(category))
4947
+ );
4874
4948
  var GovernanceProjectService = class {
4875
4949
  constructor(projectId, store, resolveDecisionContext = () => ({})) {
4876
4950
  this.projectId = projectId;
@@ -4998,14 +5072,13 @@ var GovernanceProjectService = class {
4998
5072
  };
4999
5073
  }
5000
5074
  case "read_observations":
5001
- return {
5002
- ok: true,
5075
+ return this.observationPage(
5003
5076
  requestId,
5004
- result: {
5005
- type: "observations",
5006
- observations: this.store.readObservations(this.projectId, request.taskId).filter((observation) => !reservedAuditCategory(observation.category))
5007
- }
5008
- };
5077
+ TASK_OBSERVATION_CATEGORIES,
5078
+ request.taskId,
5079
+ request.afterSequence,
5080
+ request.limit
5081
+ );
5009
5082
  case "read_evidence_candidates": {
5010
5083
  const limit = request.limit ?? GOVERNANCE_EVIDENCE_CANDIDATE_DEFAULT_PAGE_SIZE;
5011
5084
  const rows = this.store.readEvidenceCandidateObservations(this.projectId, request.taskId, {
@@ -5029,14 +5102,13 @@ var GovernanceProjectService = class {
5029
5102
  };
5030
5103
  }
5031
5104
  case "read_audit_observations":
5032
- return {
5033
- ok: true,
5105
+ return this.observationPage(
5034
5106
  requestId,
5035
- result: {
5036
- type: "observations",
5037
- observations: this.store.readObservations(this.projectId).filter((observation) => reservedAuditCategory(observation.category))
5038
- }
5039
- };
5107
+ AUDIT_OBSERVATION_CATEGORIES,
5108
+ void 0,
5109
+ request.afterSequence,
5110
+ request.limit
5111
+ );
5040
5112
  case "submit_command": {
5041
5113
  if (!this.commandBelongsToProject(request.command)) {
5042
5114
  return fail(
@@ -5098,6 +5170,31 @@ var GovernanceProjectService = class {
5098
5170
  );
5099
5171
  }
5100
5172
  }
5173
+ /**
5174
+ * Shared body for both observation reads. Fetches one row past the page so
5175
+ * the presence of a further page is known without a second query — the same
5176
+ * convention `read_evidence_candidates` uses.
5177
+ */
5178
+ observationPage(requestId, categories, taskId, afterSequence, limit) {
5179
+ const pageSize = limit ?? GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE;
5180
+ const rows = this.store.readObservationsPage({
5181
+ projectId: this.projectId,
5182
+ ...taskId === void 0 ? {} : { taskId },
5183
+ categories,
5184
+ afterSequence: afterSequence ?? 0,
5185
+ limit: pageSize + 1
5186
+ });
5187
+ const page = rows.slice(0, pageSize);
5188
+ return {
5189
+ ok: true,
5190
+ requestId,
5191
+ result: {
5192
+ type: "observations",
5193
+ observations: page,
5194
+ nextAfterSequence: rows.length > pageSize && page.length > 0 ? page.at(-1)?.sequence ?? null : null
5195
+ }
5196
+ };
5197
+ }
5101
5198
  commandBelongsToProject(command) {
5102
5199
  if (command.type === "create_task") {
5103
5200
  return command.projectId === this.projectId && command.contract.projectId === this.projectId;
@@ -5308,12 +5405,14 @@ var GovernanceProjectServer = class {
5308
5405
  else socket.destroy();
5309
5406
  }
5310
5407
  accept(socket) {
5311
- let buffer = Buffer.alloc(0);
5408
+ const chunks = [];
5409
+ let size = 0;
5312
5410
  let handled = false;
5313
5411
  socket.on("data", (chunk) => {
5314
5412
  if (handled) return;
5315
- buffer = Buffer.concat([buffer, chunk]);
5316
- if (buffer.byteLength > GOVERNANCE_IPC_MAX_FRAME_BYTES) {
5413
+ const newline = chunk.indexOf(10);
5414
+ const frameBytes = size + (newline < 0 ? chunk.byteLength : newline);
5415
+ if (frameBytes > GOVERNANCE_IPC_MAX_FRAME_BYTES) {
5317
5416
  handled = true;
5318
5417
  this.send(
5319
5418
  socket,
@@ -5321,10 +5420,14 @@ var GovernanceProjectServer = class {
5321
5420
  );
5322
5421
  return;
5323
5422
  }
5324
- const newline = buffer.indexOf(10);
5325
- if (newline < 0) return;
5423
+ if (newline < 0) {
5424
+ chunks.push(chunk);
5425
+ size = frameBytes;
5426
+ return;
5427
+ }
5326
5428
  handled = true;
5327
- const line = buffer.subarray(0, newline).toString("utf8").replace(/\r$/, "");
5429
+ chunks.push(chunk.subarray(0, newline));
5430
+ const line = Buffer.concat(chunks).toString("utf8").replace(/\r$/, "");
5328
5431
  this.handleLine(socket, line);
5329
5432
  });
5330
5433
  }
@@ -1,6 +1,6 @@
1
1
  import type { GovernanceAttachmentBrokerControllerSnapshot } from './attachment-broker-controller.js';
2
2
  import type { GovernanceCapabilityGrant, GovernanceServiceCredential } from './capability-grant.js';
3
- import type { AppendGovernanceObservationResult, GovernanceCommandExecution, GovernanceCommandReceipt, GovernanceEventStore, StoredGovernanceObservation } from './event-store.js';
3
+ import { type AppendGovernanceObservationResult, type GovernanceCommandExecution, type GovernanceCommandReceipt, type GovernanceEventStore, type StoredGovernanceObservation } from './event-store.js';
4
4
  import { type GovernanceEvidenceCandidateLedgerEntry } from './evidence-candidate.js';
5
5
  import { GOVERNANCE_SERVICE_PROTOCOL_VERSION, type GovernanceServiceClientContext, type GovernanceServiceRequest } from './service-protocol.js';
6
6
  import type { GovernanceCommand, GovernanceDecisionContext, GovernanceEvent, TaskAggregate } from './task-aggregate.js';
@@ -24,6 +24,7 @@ export type GovernanceServiceResult = {
24
24
  } | {
25
25
  readonly type: 'observations';
26
26
  readonly observations: readonly StoredGovernanceObservation[];
27
+ readonly nextAfterSequence: number | null;
27
28
  } | {
28
29
  readonly type: 'evidence_candidates';
29
30
  readonly taskId: string;
@@ -116,6 +117,12 @@ export declare class GovernanceProjectService {
116
117
  handle(request: GovernanceServiceRequest, client: GovernanceServiceClientContext): GovernanceServiceResponse;
117
118
  close(): void;
118
119
  private handleAuthorized;
120
+ /**
121
+ * Shared body for both observation reads. Fetches one row past the page so
122
+ * the presence of a further page is known without a second query — the same
123
+ * convention `read_evidence_candidates` uses.
124
+ */
125
+ private observationPage;
119
126
  private commandBelongsToProject;
120
127
  }
121
128
  //# sourceMappingURL=project-service.d.ts.map
@@ -28,6 +28,8 @@ export type GovernanceServiceRequest = GovernanceServiceRequestMetadata & ({
28
28
  } | {
29
29
  readonly type: 'read_observations';
30
30
  readonly taskId?: string | undefined;
31
+ readonly afterSequence?: number | undefined;
32
+ readonly limit?: number | undefined;
31
33
  } | {
32
34
  readonly type: 'read_evidence_candidates';
33
35
  readonly taskId: string;
@@ -35,6 +37,8 @@ export type GovernanceServiceRequest = GovernanceServiceRequestMetadata & ({
35
37
  readonly limit?: number | undefined;
36
38
  } | {
37
39
  readonly type: 'read_audit_observations';
40
+ readonly afterSequence?: number | undefined;
41
+ readonly limit?: number | undefined;
38
42
  } | {
39
43
  readonly type: 'read_own_capability_grant';
40
44
  } | {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/governance",
3
- "version": "0.298.3",
3
+ "version": "0.300.0",
4
4
  "license": "MIT",
5
5
  "description": "Deterministic workflow contracts and transition policy for WrongStack orchestration.",
6
6
  "repository": {