adhdev 0.9.81 → 0.9.82-rc.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adhdev",
3
- "version": "0.9.81",
3
+ "version": "0.9.82-rc.10",
4
4
  "description": "ADHDev — Agent Dashboard Hub for Dev. Remote-control AI coding agents from anywhere.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -47,7 +47,7 @@
47
47
  "node": ">=18"
48
48
  },
49
49
  "dependencies": {
50
- "@adhdev/daemon-core": "*",
50
+ "@adhdev/daemon-core": "0.9.82-rc.10",
51
51
  "@adhdev/ghostty-vt-node": "*",
52
52
  "@modelcontextprotocol/sdk": "^1.0.0",
53
53
  "@xterm/addon-serialize": "^0.14.0",
@@ -583,10 +583,18 @@ function isIdleSessionRecord(session) {
583
583
  function chooseDispatchableSession(sessions, providerType, meshId, nodeId) {
584
584
  const live = sessions.filter((session) => !isTerminalSessionRecord(session));
585
585
  const matchingProvider = (session) => !providerType || session?.providerType === providerType || session?.cliType === providerType;
586
+ const isMeshOwnedDelegateSession = (session) => {
587
+ const settings = session?.settings;
588
+ const sessionMeshId = typeof settings?.meshNodeFor === "string" ? settings.meshNodeFor.trim() : "";
589
+ const coordinatorDaemonId = typeof settings?.meshCoordinatorDaemonId === "string" ? settings.meshCoordinatorDaemonId.trim() : "";
590
+ const sessionNodeId = typeof settings?.meshNodeId === "string" ? settings.meshNodeId.trim() : "";
591
+ if (sessionMeshId !== meshId || !coordinatorDaemonId) return false;
592
+ return !sessionNodeId || sessionNodeId === nodeId;
593
+ };
586
594
  const meshSessions = live.filter(
587
- (session) => session?.settings?.meshNodeFor === meshId || session?.settings?.meshNodeId === nodeId
595
+ (session) => isMeshOwnedDelegateSession(session)
588
596
  );
589
- return meshSessions.find((session) => isIdleSessionRecord(session) && matchingProvider(session)) || meshSessions.find(matchingProvider) || live.find((session) => isIdleSessionRecord(session) && matchingProvider(session)) || live.find(matchingProvider) || live.find(isIdleSessionRecord) || live[0];
597
+ return meshSessions.find((session) => isIdleSessionRecord(session) && matchingProvider(session)) || meshSessions.find(matchingProvider) || void 0;
590
598
  }
591
599
  function findNestedPayload(value, predicate) {
592
600
  const seen = /* @__PURE__ */ new Set();
@@ -872,6 +880,10 @@ function summarizeRelatedRepoStatus(repo, status) {
872
880
  workspace: repo.workspace,
873
881
  isGitRepo: status?.isGitRepo === true,
874
882
  branch: status?.branch ?? null,
883
+ upstream: status?.upstream ?? null,
884
+ upstreamStatus: typeof status?.upstreamStatus === "string" ? status.upstreamStatus : status?.upstream ? "unchecked" : "no_upstream",
885
+ upstreamFetchedAt: Number.isFinite(Number(status?.upstreamFetchedAt)) ? Number(status.upstreamFetchedAt) : null,
886
+ upstreamFetchError: typeof status?.upstreamFetchError === "string" ? status.upstreamFetchError : null,
875
887
  ahead: Number.isFinite(Number(status?.ahead)) ? Number(status.ahead) : 0,
876
888
  behind: Number.isFinite(Number(status?.behind)) ? Number(status.behind) : 0,
877
889
  dirty,
@@ -888,7 +900,7 @@ async function collectRelatedRepoStatuses(ctx, node) {
888
900
  const results = [];
889
901
  for (const repo of relatedRepos) {
890
902
  try {
891
- const statusResult = !isLocalTransport(ctx.transport) && node.daemonId ? await ctx.transport.gitStatus(node.daemonId, repo.workspace, false) : await commandForNode(ctx, node, "git_status", { workspace: repo.workspace });
903
+ const statusResult = !isLocalTransport(ctx.transport) && node.daemonId ? await ctx.transport.gitStatus(node.daemonId, repo.workspace, false, true) : await commandForNode(ctx, node, "git_status", { workspace: repo.workspace, refreshUpstream: true });
892
904
  const status = extractGitStatus(statusResult);
893
905
  results.push(summarizeRelatedRepoStatus(repo, status));
894
906
  } catch (e) {
@@ -936,11 +948,13 @@ function buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges) {
936
948
  const ahead = readNumeric(status?.ahead);
937
949
  const behind = readNumeric(status?.behind);
938
950
  const upstream = readString(status?.upstream) ?? null;
951
+ const upstreamStatus = readString(status?.upstreamStatus) ?? (upstream ? "unchecked" : "no_upstream");
939
952
  const hasConflicts = status?.hasConflicts === true || Array.isArray(status?.conflictFiles) && status.conflictFiles.length > 0;
940
953
  const base = {
941
954
  defaultBranch,
942
955
  branch,
943
956
  upstream,
957
+ upstreamStatus,
944
958
  ahead,
945
959
  behind,
946
960
  isWorktree: node.isLocalWorktree === true,
@@ -974,6 +988,15 @@ function buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges) {
974
988
  };
975
989
  }
976
990
  if (branch === defaultBranch) {
991
+ if (upstream && upstreamStatus !== "fresh") {
992
+ return {
993
+ ...base,
994
+ status: "blocked_review",
995
+ needsConvergence: true,
996
+ reason: "default_branch_upstream_unverified",
997
+ nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${node.id}'.`
998
+ };
999
+ }
977
1000
  if (ahead > 0 || behind > 0) {
978
1001
  return {
979
1002
  ...base,
@@ -1000,6 +1023,15 @@ function buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges) {
1000
1023
  nextStep: `Run mesh_refine_node(node_id: "${node.id}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`
1001
1024
  };
1002
1025
  }
1026
+ if (upstream && upstreamStatus !== "fresh") {
1027
+ return {
1028
+ ...base,
1029
+ status: "blocked_review",
1030
+ needsConvergence: true,
1031
+ reason: "feature_branch_upstream_unverified",
1032
+ nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`
1033
+ };
1034
+ }
1003
1035
  if (!upstream || ahead > 0 || behind > 0) {
1004
1036
  return {
1005
1037
  ...base,
@@ -1043,6 +1075,71 @@ async function commandForNode(ctx, node, command, args = {}) {
1043
1075
  }
1044
1076
  throw new Error(`Command '${command}' requires daemon IPC/local transport for node '${node.id}'`);
1045
1077
  }
1078
+ function normalizePendingMeshCoordinatorEvents(value) {
1079
+ const payload = unwrapCommandPayload(value);
1080
+ const events = Array.isArray(payload?.events) ? payload.events : Array.isArray(value?.events) ? value.events : [];
1081
+ return events.filter((event) => event && typeof event === "object");
1082
+ }
1083
+ function buildMeshForwardPayloadFromPendingEvent(event) {
1084
+ const metadataEvent = event?.metadataEvent && typeof event.metadataEvent === "object" ? event.metadataEvent : {};
1085
+ return {
1086
+ event: readString(event?.event),
1087
+ meshId: readString(event?.meshId),
1088
+ nodeId: readString(event?.nodeId) || readString(metadataEvent.meshNodeId),
1089
+ workspace: readString(event?.workspace) || readString(metadataEvent.workspace),
1090
+ targetSessionId: readString(metadataEvent.targetSessionId) || readString(metadataEvent.sessionId) || readString(metadataEvent.instanceId),
1091
+ providerType: readString(metadataEvent.providerType),
1092
+ providerSessionId: readString(metadataEvent.providerSessionId),
1093
+ finalSummary: readString(metadataEvent.finalSummary) || readString(metadataEvent.summary),
1094
+ ...metadataEvent.intentional === true ? { intentional: true } : {},
1095
+ ...metadataEvent.intentionalStop === true ? { intentionalStop: true } : {},
1096
+ ...metadataEvent.operatorCleanup === true ? { operatorCleanup: true } : {},
1097
+ ...readString(metadataEvent.reason) ? { reason: readString(metadataEvent.reason) } : {},
1098
+ ...readString(metadataEvent.stopReason) ? { stopReason: readString(metadataEvent.stopReason) } : {},
1099
+ ...readString(metadataEvent.cleanupReason) ? { cleanupReason: readString(metadataEvent.cleanupReason) } : {},
1100
+ ...readString(metadataEvent.source) ? { source: readString(metadataEvent.source) } : {}
1101
+ };
1102
+ }
1103
+ async function drainCoordinatorPendingEvents(ctx, opts) {
1104
+ const requestedNodeIds = opts?.nodeIds?.length ? new Set(opts.nodeIds) : null;
1105
+ const matchesCurrentMesh = (event) => readString(event?.meshId) === ctx.mesh.id;
1106
+ if (ctx.transport instanceof IpcTransport) {
1107
+ const surfacedEvents = [];
1108
+ try {
1109
+ surfacedEvents.push(
1110
+ ...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", {})).filter(matchesCurrentMesh)
1111
+ );
1112
+ } catch {
1113
+ }
1114
+ for (const node of ctx.mesh.nodes) {
1115
+ if (!node.daemonId || isLocalControlPlaneNode(ctx, node)) continue;
1116
+ if (requestedNodeIds && !requestedNodeIds.has(node.id)) continue;
1117
+ try {
1118
+ const remoteEvents = normalizePendingMeshCoordinatorEvents(
1119
+ await ctx.transport.meshCommand(node.daemonId, "get_pending_mesh_events", {})
1120
+ ).filter(matchesCurrentMesh);
1121
+ if (remoteEvents.length === 0) continue;
1122
+ for (const event of remoteEvents) {
1123
+ const payload = buildMeshForwardPayloadFromPendingEvent(event);
1124
+ if (!payload.event || !payload.meshId) continue;
1125
+ await ctx.transport.command("mesh_forward_event", payload);
1126
+ }
1127
+ } catch {
1128
+ }
1129
+ }
1130
+ try {
1131
+ surfacedEvents.push(
1132
+ ...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", {})).filter(matchesCurrentMesh)
1133
+ );
1134
+ } catch {
1135
+ }
1136
+ return surfacedEvents;
1137
+ }
1138
+ if (isLocalTransport(ctx.transport)) {
1139
+ return (0, import_daemon_core.drainPendingMeshCoordinatorEvents)().filter(matchesCurrentMesh);
1140
+ }
1141
+ return [];
1142
+ }
1046
1143
  function isP2pTransportUnavailableError(error) {
1047
1144
  return (0, import_daemon_core.isP2pRelayTransportFailure)(error);
1048
1145
  }
@@ -1344,7 +1441,7 @@ async function meshStatus(ctx) {
1344
1441
  };
1345
1442
  try {
1346
1443
  if (!isLocalTransport(transport) && node.daemonId) {
1347
- const result = await transport.gitStatus(node.daemonId, node.workspace, false);
1444
+ const result = await transport.gitStatus(node.daemonId, node.workspace, false, true);
1348
1445
  const status = extractGitStatus(result);
1349
1446
  const uncommittedChanges = countUncommittedChanges(status);
1350
1447
  const dirty = isGitStatusDirty(status);
@@ -1362,6 +1459,7 @@ async function meshStatus(ctx) {
1362
1459
  const autoDiscover = node.policy?.autoDiscoverSubmodules !== false;
1363
1460
  const statusResult = await commandForNode(ctx, node, "git_status", {
1364
1461
  workspace: node.workspace,
1462
+ refreshUpstream: true,
1365
1463
  includeSubmodules: autoDiscover,
1366
1464
  submoduleIgnorePaths: node.policy?.submoduleIgnorePaths || void 0
1367
1465
  });
@@ -1460,13 +1558,7 @@ async function meshStatus(ctx) {
1460
1558
  } catch {
1461
1559
  }
1462
1560
  try {
1463
- let pendingEvents = [];
1464
- if (ctx.transport instanceof IpcTransport) {
1465
- const eventsResult = await ctx.transport.command("get_pending_mesh_events", {});
1466
- pendingEvents = Array.isArray(eventsResult?.events) ? eventsResult.events : [];
1467
- } else if (isLocalTransport(ctx.transport)) {
1468
- pendingEvents = (0, import_daemon_core.drainPendingMeshCoordinatorEvents)();
1469
- }
1561
+ const pendingEvents = await drainCoordinatorPendingEvents(ctx);
1470
1562
  if (pendingEvents.length > 0) {
1471
1563
  response.pendingCoordinatorEvents = pendingEvents;
1472
1564
  }
@@ -1476,6 +1568,7 @@ async function meshStatus(ctx) {
1476
1568
  }
1477
1569
  async function meshTaskHistory(ctx, args) {
1478
1570
  const { mesh } = ctx;
1571
+ await drainCoordinatorPendingEvents(ctx);
1479
1572
  const tail = typeof args.tail === "number" && args.tail > 0 ? args.tail : 20;
1480
1573
  const kind = typeof args.kind === "string" && args.kind.trim() ? [args.kind.trim()] : void 0;
1481
1574
  const entries = (0, import_daemon_core.readLedgerEntries)(mesh.id, { tail, kind });
@@ -1823,6 +1916,9 @@ async function meshReadChat(ctx, args) {
1823
1916
  if (!node) {
1824
1917
  return JSON.stringify(buildMissingNodeReadChatRecovery(ctx, args), null, 2);
1825
1918
  }
1919
+ if (ctx.transport instanceof IpcTransport || isLocalTransport(ctx.transport)) {
1920
+ await drainCoordinatorPendingEvents(ctx, { nodeIds: [args.node_id] });
1921
+ }
1826
1922
  if (isLocalTransport(ctx.transport)) {
1827
1923
  const cached = meshSessionProviderMetadata.get(meshSessionCacheKey(args.node_id, args.session_id));
1828
1924
  const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
@@ -2028,7 +2124,7 @@ async function meshGitStatus(ctx, args) {
2028
2124
  const submoduleIgnorePaths = node.policy?.submoduleIgnorePaths || [];
2029
2125
  try {
2030
2126
  if (!isLocalTransport(ctx.transport) && node.daemonId) {
2031
- const result = await ctx.transport.gitStatus(node.daemonId, node.workspace, true);
2127
+ const result = await ctx.transport.gitStatus(node.daemonId, node.workspace, true, true);
2032
2128
  return JSON.stringify({
2033
2129
  nodeId: args.node_id,
2034
2130
  workspace: node.workspace,
@@ -2040,6 +2136,7 @@ async function meshGitStatus(ctx, args) {
2040
2136
  } else if (isLocalTransport(ctx.transport)) {
2041
2137
  const statusResult = await commandForNode(ctx, node, "git_status", {
2042
2138
  workspace: node.workspace,
2139
+ refreshUpstream: true,
2043
2140
  includeSubmodules: autoDiscoverSubmodules,
2044
2141
  submoduleIgnorePaths: submoduleIgnorePaths.length > 0 ? submoduleIgnorePaths : void 0
2045
2142
  });
@@ -2511,8 +2608,8 @@ var CloudTransport = class {
2511
2608
  if (!res.ok) throw new Error(`Approve failed: ${res.status}`);
2512
2609
  return res.json();
2513
2610
  }
2514
- async gitStatus(daemonId, workspace, includeDiff = true) {
2515
- const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff) });
2611
+ async gitStatus(daemonId, workspace, includeDiff = true, refreshUpstream = false) {
2612
+ const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff), refreshUpstream: String(refreshUpstream) });
2516
2613
  const res = await fetch(
2517
2614
  `${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-status?${params}`,
2518
2615
  { headers: this.headers() }