@rynx-ai/daemon 0.1.11-beta.37 → 0.1.11-beta.39

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/plugin-channel-lark",
3
- "version": "0.1.11-beta.37",
3
+ "version": "0.1.11-beta.39",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
package/dist/db.js CHANGED
@@ -100,6 +100,7 @@ function migrate(conn) {
100
100
  status TEXT NOT NULL,
101
101
  data TEXT NOT NULL,
102
102
  created_by TEXT,
103
+ parent_tool_call_id TEXT,
103
104
  created_at INTEGER NOT NULL,
104
105
  UNIQUE(session_id, position)
105
106
  );
@@ -370,6 +371,7 @@ function migrate(conn) {
370
371
  addColumnIfMissing(conn, "session_message_operations", "response_id", "TEXT");
371
372
  addColumnIfMissing(conn, "session_message_operations", "message_item_id", "TEXT");
372
373
  addColumnIfMissing(conn, "session_message_operations", "execution_snapshot", "TEXT");
374
+ addColumnIfMissing(conn, "session_items", "parent_tool_call_id", "TEXT");
373
375
  addColumnIfMissing(conn, "session_portal_tickets", "grant_handle", "TEXT");
374
376
  addColumnIfMissing(conn, "session_portal_tickets", "embed_origin", "TEXT");
375
377
  addColumnIfMissing(conn, "session_portal_grants", "grant_handle", "TEXT");
@@ -42,6 +42,14 @@ const NETWORK_AUTO_ATTACH_FILTER = [
42
42
  { type: "shared_worker" },
43
43
  { exclude: true },
44
44
  ];
45
+ const NETWORK_AUTO_ATTACH_PARAMS = {
46
+ autoAttach: true,
47
+ // Pause new targets until Fetch interception is installed. This is required
48
+ // for Session Headers to cover a popup's very first navigation request.
49
+ waitForDebuggerOnStart: true,
50
+ flatten: true,
51
+ filter: NETWORK_AUTO_ATTACH_FILTER,
52
+ };
45
53
  export function createHeadlessBrowserHost(options = {}) {
46
54
  return new HeadlessBrowserHost(options);
47
55
  }
@@ -235,12 +243,7 @@ class HeadlessBrowserHandle {
235
243
  }
236
244
  async initialize() {
237
245
  await this.host.connection.command("Target.setDiscoverTargets", { discover: true });
238
- await this.host.connection.command("Target.setAutoAttach", {
239
- autoAttach: true,
240
- waitForDebuggerOnStart: true,
241
- flatten: true,
242
- filter: NETWORK_AUTO_ATTACH_FILTER,
243
- });
246
+ await this.host.connection.command("Target.setAutoAttach", NETWORK_AUTO_ATTACH_PARAMS);
244
247
  await this.createFreshStartupPage();
245
248
  await this.refreshTargets();
246
249
  const requestedUrl = this.host.input.url;
@@ -433,14 +436,28 @@ class HeadlessBrowserHandle {
433
436
  this.closing = true;
434
437
  this.closePromise = (async () => {
435
438
  await Promise.allSettled([...this.surfaceSources.values()].map((source) => source.close()));
436
- this.host.connection.close();
437
439
  let terminated = false;
438
440
  try {
439
- await terminateProcessGroup(this.host.child, this.host.exit, this.host.terminationGraceMs);
441
+ let exited = false;
442
+ if (this.host.connection.connected) {
443
+ const closeCommand = this.host.connection.command("Browser.close")
444
+ .catch(() => undefined);
445
+ exited = await Promise.race([
446
+ this.host.exit.then(() => true),
447
+ delay(this.host.terminationGraceMs).then(() => false),
448
+ ]);
449
+ if (!exited)
450
+ this.host.connection.close();
451
+ await closeCommand;
452
+ }
453
+ if (!exited) {
454
+ await terminateProcessGroup(this.host.child, this.host.exit, this.host.terminationGraceMs);
455
+ }
440
456
  removeMatchingProcessLease(this.host.profileDir, this.host.ownerToken);
441
457
  terminated = true;
442
458
  }
443
459
  finally {
460
+ this.host.connection.close();
444
461
  if (terminated)
445
462
  this.host.release();
446
463
  this.eventQueue.end();
@@ -501,11 +518,19 @@ class HeadlessBrowserHandle {
501
518
  try {
502
519
  const targets = await this.readPageTargets();
503
520
  const next = new Map();
504
- for (const target of targets) {
505
- if (next.size >= RUNTIME_BROWSER_MAX_PAGES) {
506
- throw new SessionBrowserHostError("capacity", "Headless Browser has too many Pages");
521
+ const orderedTargets = [
522
+ ...targets.filter((target) => this.targets.has(target.targetId)),
523
+ ...targets.filter((target) => !this.targets.has(target.targetId)),
524
+ ];
525
+ for (const target of orderedTargets) {
526
+ if (this.closingTargetIds.has(target.targetId))
527
+ continue;
528
+ if (next.size < RUNTIME_BROWSER_MAX_PAGES) {
529
+ next.set(target.targetId, target);
530
+ continue;
507
531
  }
508
- next.set(target.targetId, target);
532
+ this.closingTargetIds.add(target.targetId);
533
+ await this.networkExclusive(() => this.closeRejectedTarget(undefined, target, false));
509
534
  }
510
535
  for (const targetId of this.targets.keys()) {
511
536
  if (!next.has(targetId))
@@ -700,6 +725,13 @@ class HeadlessBrowserHandle {
700
725
  this.targetBySession.set(sessionId, target.targetId);
701
726
  this.networkTargetTypeBySession.set(sessionId, target.type);
702
727
  if (target.type === "page") {
728
+ if (this.closingTargetIds.has(target.targetId) ||
729
+ (!this.targets.has(target.targetId) &&
730
+ this.targets.size >= RUNTIME_BROWSER_MAX_PAGES)) {
731
+ this.closingTargetIds.add(target.targetId);
732
+ void this.networkExclusive(() => this.closeRejectedTarget(sessionId, target, waitingForDebugger)).catch(() => undefined);
733
+ return;
734
+ }
703
735
  this.targets.set(target.targetId, target);
704
736
  let pending;
705
737
  pending = this.networkExclusive(() => this.initializeAttachedTarget(sessionId, target, waitingForDebugger))
@@ -744,6 +776,16 @@ class HeadlessBrowserHandle {
744
776
  const target = parseTargetInfo(event.params.targetInfo);
745
777
  if (target.type !== "page")
746
778
  return;
779
+ if (event.method === "Target.targetCreated" &&
780
+ !this.targets.has(target.targetId) &&
781
+ this.targets.size >= RUNTIME_BROWSER_MAX_PAGES) {
782
+ this.closingTargetIds.add(target.targetId);
783
+ void this.networkExclusive(() => this.closeRejectedTarget(undefined, target, false))
784
+ .catch(() => undefined);
785
+ return;
786
+ }
787
+ if (this.closingTargetIds.has(target.targetId))
788
+ return;
747
789
  this.targets.set(target.targetId, target);
748
790
  if (event.method === "Target.targetCreated")
749
791
  this.activeTargetId = target.targetId;
@@ -765,7 +807,12 @@ class HeadlessBrowserHandle {
765
807
  if (event.method === "Target.targetCrashed") {
766
808
  const targetId = optionalCdpId(event.params.targetId);
767
809
  if (targetId && this.targets.has(targetId)) {
768
- this.markUnavailable("A Headless Browser Page renderer crashed");
810
+ const target = this.targets.get(targetId);
811
+ this.closingTargetIds.add(targetId);
812
+ this.dropTarget(targetId);
813
+ void this.networkExclusive(() => this.closeRejectedTarget(undefined, target, false))
814
+ .catch(() => undefined);
815
+ this.eventQueue.changed(this.browserGeneration);
769
816
  }
770
817
  return;
771
818
  }
@@ -778,6 +825,10 @@ class HeadlessBrowserHandle {
778
825
  const targetId = event.sessionId ? this.targetBySession.get(event.sessionId) : undefined;
779
826
  if (!targetId)
780
827
  return;
828
+ if (event.method === "Page.javascriptDialogOpening") {
829
+ void this.dismissJavaScriptDialog(event.sessionId, targetId);
830
+ return;
831
+ }
781
832
  if (event.method === "Inspector.detached") {
782
833
  this.dropPageSession(event.sessionId);
783
834
  return;
@@ -856,17 +907,14 @@ class HeadlessBrowserHandle {
856
907
  }
857
908
  async initializeAttachedTarget(sessionId, target, waitingForDebugger) {
858
909
  try {
859
- await this.host.connection.command("Network.enable", {}, sessionId);
910
+ // Fetch.enable succeeds while a waitForDebugger target is paused and must
911
+ // precede its first request. Network.enable does not: current Chrome for
912
+ // Testing builds leave that command pending until the target is resumed.
860
913
  await this.host.connection.command("Fetch.enable", {
861
914
  patterns: [{ urlPattern: "*", requestStage: "Request" }],
862
915
  }, sessionId);
863
916
  if (target.type === "page" || target.type === "iframe") {
864
- await this.host.connection.command("Target.setAutoAttach", {
865
- autoAttach: true,
866
- waitForDebuggerOnStart: true,
867
- flatten: true,
868
- filter: NETWORK_AUTO_ATTACH_FILTER,
869
- }, sessionId);
917
+ await this.host.connection.command("Target.setAutoAttach", NETWORK_AUTO_ATTACH_PARAMS, sessionId);
870
918
  }
871
919
  if (target.type === "page") {
872
920
  await this.host.connection.command("Page.enable", {}, sessionId);
@@ -874,14 +922,52 @@ class HeadlessBrowserHandle {
874
922
  if (waitingForDebugger) {
875
923
  await this.host.connection.command("Runtime.runIfWaitingForDebugger", {}, sessionId);
876
924
  }
925
+ await this.host.connection.command("Network.enable", {}, sessionId);
877
926
  }
878
927
  catch (error) {
879
928
  if (isMissingTargetError(error) || isTransientPageTransitionError(error)) {
880
929
  this.dropAttachedSession(sessionId);
881
930
  throw missingPage(error);
882
931
  }
883
- this.markUnavailable("Headless Browser could not initialize a network target");
884
- throw this.mapOperationError(error);
932
+ if (!this.host.connection.connected || error instanceof CdpDisconnectedError) {
933
+ this.markUnavailable("The native CDP observer disconnected");
934
+ throw this.mapOperationError(error);
935
+ }
936
+ await this.closeRejectedTarget(sessionId, target, waitingForDebugger);
937
+ if (target.type === "page")
938
+ throw missingPage(error);
939
+ throw unavailable("Headless Browser isolated a network target that failed setup", error);
940
+ }
941
+ }
942
+ async closeRejectedTarget(sessionId, target, waitingForDebugger) {
943
+ if (target.type === "page") {
944
+ this.closingTargetIds.add(target.targetId);
945
+ this.dropTarget(target.targetId);
946
+ }
947
+ else if (sessionId) {
948
+ this.dropAttachedSession(sessionId);
949
+ }
950
+ let closed = false;
951
+ try {
952
+ if (this.host.connection.connected) {
953
+ const result = await this.host.connection.command("Target.closeTarget", {
954
+ targetId: target.targetId,
955
+ });
956
+ closed = result.success === true;
957
+ }
958
+ }
959
+ catch (error) {
960
+ if (!isMissingTargetError(error) && !isTransientPageTransitionError(error)) {
961
+ if (!this.host.connection.connected || error instanceof CdpDisconnectedError) {
962
+ this.markUnavailable("The native CDP observer disconnected");
963
+ }
964
+ }
965
+ }
966
+ if (!closed && waitingForDebugger && sessionId && this.host.connection.connected) {
967
+ await this.host.connection.command("Runtime.runIfWaitingForDebugger", {}, sessionId).catch(() => undefined);
968
+ }
969
+ if (!closed && sessionId && this.host.connection.connected) {
970
+ await this.host.connection.command("Target.detachFromTarget", { sessionId }).catch(() => undefined);
885
971
  }
886
972
  }
887
973
  async detachAuxiliaryPageSession(sessionId) {
@@ -898,6 +984,27 @@ class HeadlessBrowserHandle {
898
984
  this.dropAttachedSession(sessionId);
899
985
  }
900
986
  }
987
+ async dismissJavaScriptDialog(sessionId, targetId) {
988
+ try {
989
+ // Rynx has no native dialog presentation surface. Dismiss every modal
990
+ // deterministically so alert/confirm/prompt/beforeunload cannot block all
991
+ // later CDP commands for this Page.
992
+ await this.host.connection.command("Page.handleJavaScriptDialog", { accept: false }, sessionId);
993
+ }
994
+ catch (error) {
995
+ if (isMissingTargetError(error) || isTransientPageTransitionError(error)) {
996
+ this.dropAttachedSession(sessionId);
997
+ return;
998
+ }
999
+ if (!this.host.connection.connected || error instanceof CdpDisconnectedError) {
1000
+ this.markUnavailable("The native CDP observer disconnected");
1001
+ return;
1002
+ }
1003
+ const target = this.targets.get(targetId);
1004
+ if (target)
1005
+ await this.closeRejectedTarget(sessionId, target, false);
1006
+ }
1007
+ }
901
1008
  async continuePausedRequest(sessionId, params) {
902
1009
  const requestId = requiredCdpId(params.requestId, "Fetch.requestPaused requestId");
903
1010
  const config = this.requestHeaders;
@@ -194,10 +194,10 @@ export class SqliteSessionForkStore {
194
194
  }
195
195
  const insertItem = conn.prepare(`INSERT INTO session_items (
196
196
  id, session_id, position, response_id, type, status,
197
- data, created_by, created_at
197
+ data, created_by, parent_tool_call_id, created_at
198
198
  ) VALUES (
199
199
  @id, @session_id, @position, @response_id, @type, @status,
200
- @data, @created_by, @created_at
200
+ @data, @created_by, @parent_tool_call_id, @created_at
201
201
  )`);
202
202
  for (const item of clonedItems) {
203
203
  insertItem.run({
@@ -209,6 +209,7 @@ export class SqliteSessionForkStore {
209
209
  status: item.status,
210
210
  data: JSON.stringify(item.data),
211
211
  created_by: item.createdBy ?? null,
212
+ parent_tool_call_id: item.parentToolCallId ?? null,
212
213
  created_at: item.createdAt,
213
214
  });
214
215
  }
@@ -1,5 +1,6 @@
1
+ import { SessionLogAppendError } from "@rynx-ai/core";
1
2
  import { db } from "./db.js";
2
- import { transaction } from "./sqlite.js";
3
+ import { hasSqlitePrimaryCode, SQLITE_CONSTRAINT, transaction } from "./sqlite.js";
3
4
  function toItem(row) {
4
5
  return {
5
6
  id: row.id,
@@ -9,6 +10,9 @@ function toItem(row) {
9
10
  status: row.status,
10
11
  createdAt: row.created_at,
11
12
  ...(row.created_by ? { createdBy: row.created_by } : {}),
13
+ ...(row.parent_tool_call_id
14
+ ? { parentToolCallId: row.parent_tool_call_id }
15
+ : {}),
12
16
  type: row.type,
13
17
  data: JSON.parse(row.data),
14
18
  };
@@ -19,8 +23,10 @@ export class SqliteSessionLogStore {
19
23
  return [];
20
24
  const conn = db();
21
25
  const insert = conn.prepare(`INSERT INTO session_items
22
- (id, session_id, position, response_id, type, status, data, created_by, created_at)
23
- VALUES (@id, @session_id, @position, @response_id, @type, @status, @data, @created_by, @created_at)`);
26
+ (id, session_id, position, response_id, type, status, data, created_by,
27
+ parent_tool_call_id, created_at)
28
+ VALUES (@id, @session_id, @position, @response_id, @type, @status, @data,
29
+ @created_by, @parent_tool_call_id, @created_at)`);
24
30
  const maxStmt = conn.prepare("SELECT MAX(position) AS max FROM session_items WHERE session_id = ?");
25
31
  const tx = transaction(conn, (batch) => {
26
32
  const { max } = maxStmt.get(sessionId);
@@ -36,6 +42,7 @@ export class SqliteSessionLogStore {
36
42
  status: stored.status,
37
43
  data: JSON.stringify(stored.data),
38
44
  created_by: stored.createdBy ?? null,
45
+ parent_tool_call_id: stored.parentToolCallId ?? null,
39
46
  created_at: stored.createdAt,
40
47
  });
41
48
  const resourceIds = stored.type === "message" && stored.data.role === "user"
@@ -54,10 +61,14 @@ export class SqliteSessionLogStore {
54
61
  const resource = conn.prepare(`SELECT state, message_item_id
55
62
  FROM session_resources
56
63
  WHERE id = ? AND session_id = ?`).get(resourceId, sessionId);
57
- if (!resource ||
58
- resource.state === "uploading" ||
59
- (resource.message_item_id && resource.message_item_id !== stored.id)) {
60
- throw new Error(`Session resource ${resourceId} is not committable`);
64
+ if (!resource) {
65
+ throw new SessionLogAppendError(`Session resource ${resourceId} does not exist`, 422, "session_resource_missing");
66
+ }
67
+ if (resource.state === "uploading") {
68
+ throw new SessionLogAppendError(`Session resource ${resourceId} is still uploading`, 409, "session_resource_not_ready");
69
+ }
70
+ if (resource.message_item_id && resource.message_item_id !== stored.id) {
71
+ throw new SessionLogAppendError(`Session resource ${resourceId} is already committed to another item`, 422, "session_resource_already_committed");
61
72
  }
62
73
  conn.prepare(`UPDATE session_resources
63
74
  SET state = 'committed', message_item_id = ?,
@@ -68,7 +79,17 @@ export class SqliteSessionLogStore {
68
79
  return stored;
69
80
  });
70
81
  });
71
- return tx(items);
82
+ try {
83
+ return tx(items);
84
+ }
85
+ catch (error) {
86
+ if (error instanceof SessionLogAppendError)
87
+ throw error;
88
+ if (hasSqlitePrimaryCode(error, SQLITE_CONSTRAINT)) {
89
+ throw new SessionLogAppendError(error instanceof Error ? error.message : "Session log constraint rejected the item", 422, "session_log_constraint", { cause: error });
90
+ }
91
+ throw error;
92
+ }
72
93
  }
73
94
  async list(sessionId, opts = {}) {
74
95
  const conn = db();
@@ -1008,7 +1008,7 @@ function invalid(message) {
1008
1008
  return new MachineSessionServiceFailure("failed_precondition", message);
1009
1009
  }
1010
1010
  function unavailable(message) {
1011
- return new MachineSessionServiceFailure("failed_precondition", message);
1011
+ return new MachineSessionServiceFailure("failed_precondition", message, undefined, undefined, true);
1012
1012
  }
1013
1013
  function conflict(message) {
1014
1014
  return new MachineSessionServiceFailure("conflict", message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/daemon",
3
- "version": "0.1.11-beta.37",
3
+ "version": "0.1.11-beta.39",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -51,17 +51,18 @@
51
51
  "tar": "^7.5.19",
52
52
  "undici": "^7.28.0",
53
53
  "ws": "^8.21.0",
54
- "@rynx-ai/plugin-runner": "0.1.11-beta.37",
55
- "@rynx-ai/emulator": "0.1.11-beta.37",
56
- "@rynx-ai/core": "0.1.11-beta.37",
57
- "@rynx-ai/plugin-sdk": "0.1.11-beta.37",
58
- "@rynx-ai/protocol": "0.1.11-beta.37",
59
- "@rynx-ai/remote-runtime-client": "0.1.11-beta.37",
60
- "@rynx-ai/server": "0.1.11-beta.37"
54
+ "@rynx-ai/core": "0.1.11-beta.39",
55
+ "@rynx-ai/emulator": "0.1.11-beta.39",
56
+ "@rynx-ai/plugin-runner": "0.1.11-beta.39",
57
+ "@rynx-ai/plugin-sdk": "0.1.11-beta.39",
58
+ "@rynx-ai/protocol": "0.1.11-beta.39",
59
+ "@rynx-ai/remote-runtime-client": "0.1.11-beta.39",
60
+ "@rynx-ai/server": "0.1.11-beta.39"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/ws": "^8.18.1",
64
- "@rynx-ai/plugin-channel-lark": "0.1.11-beta.37"
64
+ "@rynx-ai/browser-cdp": "0.1.11-beta.39",
65
+ "@rynx-ai/plugin-channel-lark": "0.1.11-beta.39"
65
66
  },
66
67
  "scripts": {
67
68
  "build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/index-daemon.js && node ../../scripts/stage-bundled-plugins.mjs",