@geminixiang/mikan 1.0.0-beta.20 → 1.0.0-beta.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 (292) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +18 -11
  3. package/dist/adapters/buffered-responder.d.ts +60 -0
  4. package/dist/adapters/buffered-responder.d.ts.map +1 -0
  5. package/dist/adapters/buffered-responder.js +201 -0
  6. package/dist/adapters/buffered-responder.js.map +1 -0
  7. package/dist/adapters/discord/context.d.ts.map +1 -1
  8. package/dist/adapters/discord/context.js +42 -177
  9. package/dist/adapters/discord/context.js.map +1 -1
  10. package/dist/adapters/github/bot.d.ts +14 -76
  11. package/dist/adapters/github/bot.d.ts.map +1 -1
  12. package/dist/adapters/github/bot.js +52 -443
  13. package/dist/adapters/github/bot.js.map +1 -1
  14. package/dist/adapters/github/client.d.ts +4 -0
  15. package/dist/adapters/github/client.d.ts.map +1 -1
  16. package/dist/adapters/github/client.js +5 -0
  17. package/dist/adapters/github/client.js.map +1 -1
  18. package/dist/adapters/github/context.d.ts.map +1 -1
  19. package/dist/adapters/github/context.js +32 -104
  20. package/dist/adapters/github/context.js.map +1 -1
  21. package/dist/adapters/github/github-ops.d.ts +90 -0
  22. package/dist/adapters/github/github-ops.d.ts.map +1 -0
  23. package/dist/adapters/github/github-ops.js +405 -0
  24. package/dist/adapters/github/github-ops.js.map +1 -0
  25. package/dist/adapters/github/repo.d.ts +2 -0
  26. package/dist/adapters/github/repo.d.ts.map +1 -1
  27. package/dist/adapters/github/repo.js +5 -0
  28. package/dist/adapters/github/repo.js.map +1 -1
  29. package/dist/adapters/github/tools/checks.d.ts.map +1 -1
  30. package/dist/adapters/github/tools/checks.js +6 -17
  31. package/dist/adapters/github/tools/checks.js.map +1 -1
  32. package/dist/adapters/github/tools/issue.d.ts.map +1 -1
  33. package/dist/adapters/github/tools/issue.js +6 -17
  34. package/dist/adapters/github/tools/issue.js.map +1 -1
  35. package/dist/adapters/github/tools/pr.d.ts.map +1 -1
  36. package/dist/adapters/github/tools/pr.js +6 -17
  37. package/dist/adapters/github/tools/pr.js.map +1 -1
  38. package/dist/adapters/github/tools/read.d.ts.map +1 -1
  39. package/dist/adapters/github/tools/read.js +6 -17
  40. package/dist/adapters/github/tools/read.js.map +1 -1
  41. package/dist/adapters/github/tools/review-reply.d.ts.map +1 -1
  42. package/dist/adapters/github/tools/review-reply.js +6 -17
  43. package/dist/adapters/github/tools/review-reply.js.map +1 -1
  44. package/dist/adapters/github/tools/sync.d.ts.map +1 -1
  45. package/dist/adapters/github/tools/sync.js +6 -17
  46. package/dist/adapters/github/tools/sync.js.map +1 -1
  47. package/dist/adapters/slack/bot.js +1 -1
  48. package/dist/adapters/slack/bot.js.map +1 -1
  49. package/dist/adapters/telegram/context.d.ts.map +1 -1
  50. package/dist/adapters/telegram/context.js +33 -151
  51. package/dist/adapters/telegram/context.js.map +1 -1
  52. package/dist/agent.d.ts +0 -1
  53. package/dist/agent.d.ts.map +1 -1
  54. package/dist/agent.js +28 -32
  55. package/dist/agent.js.map +1 -1
  56. package/dist/cli/arg-grammar.d.ts +25 -0
  57. package/dist/cli/arg-grammar.d.ts.map +1 -0
  58. package/dist/cli/arg-grammar.js +59 -0
  59. package/dist/cli/arg-grammar.js.map +1 -0
  60. package/dist/cli/boot.d.ts +16 -0
  61. package/dist/cli/boot.d.ts.map +1 -0
  62. package/dist/cli/boot.js +119 -0
  63. package/dist/cli/boot.js.map +1 -0
  64. package/dist/cli/ext.js +9 -11
  65. package/dist/cli/ext.js.map +1 -1
  66. package/dist/commands/extensions.js +2 -2
  67. package/dist/commands/extensions.js.map +1 -1
  68. package/dist/commands/login.d.ts.map +1 -1
  69. package/dist/commands/login.js +5 -2
  70. package/dist/commands/login.js.map +1 -1
  71. package/dist/commands/model.d.ts.map +1 -1
  72. package/dist/commands/model.js +8 -8
  73. package/dist/commands/model.js.map +1 -1
  74. package/dist/commands/sandbox.d.ts.map +1 -1
  75. package/dist/commands/sandbox.js +7 -5
  76. package/dist/commands/sandbox.js.map +1 -1
  77. package/dist/config.d.ts +0 -2
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/config.js +8 -41
  80. package/dist/config.js.map +1 -1
  81. package/dist/env-manifest.d.ts +35 -0
  82. package/dist/env-manifest.d.ts.map +1 -0
  83. package/dist/env-manifest.js +225 -0
  84. package/dist/env-manifest.js.map +1 -0
  85. package/dist/events.d.ts +0 -10
  86. package/dist/events.d.ts.map +1 -1
  87. package/dist/events.js +9 -82
  88. package/dist/events.js.map +1 -1
  89. package/dist/execution-resolver.d.ts +1 -0
  90. package/dist/execution-resolver.d.ts.map +1 -1
  91. package/dist/execution-resolver.js +33 -36
  92. package/dist/execution-resolver.js.map +1 -1
  93. package/dist/harness/event-format.d.ts +66 -0
  94. package/dist/harness/event-format.d.ts.map +1 -0
  95. package/dist/harness/event-format.js +112 -0
  96. package/dist/harness/event-format.js.map +1 -0
  97. package/dist/harness/extensions/loader.d.ts +9 -6
  98. package/dist/harness/extensions/loader.d.ts.map +1 -1
  99. package/dist/harness/extensions/loader.js +35 -50
  100. package/dist/harness/extensions/loader.js.map +1 -1
  101. package/dist/harness/extensions/types.d.ts +10 -14
  102. package/dist/harness/extensions/types.d.ts.map +1 -1
  103. package/dist/harness/extensions/types.js.map +1 -1
  104. package/dist/harness/index.d.ts +1 -0
  105. package/dist/harness/index.d.ts.map +1 -1
  106. package/dist/harness/index.js +1 -0
  107. package/dist/harness/index.js.map +1 -1
  108. package/dist/harness/runner.d.ts +16 -0
  109. package/dist/harness/runner.d.ts.map +1 -1
  110. package/dist/harness/runner.js +27 -1
  111. package/dist/harness/runner.js.map +1 -1
  112. package/dist/harness/subagent-runner.d.ts +18 -1
  113. package/dist/harness/subagent-runner.d.ts.map +1 -1
  114. package/dist/harness/subagent-runner.js +62 -30
  115. package/dist/harness/subagent-runner.js.map +1 -1
  116. package/dist/index.d.ts +2 -2
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +2 -2
  119. package/dist/index.js.map +1 -1
  120. package/dist/main.js +69 -121
  121. package/dist/main.js.map +1 -1
  122. package/dist/observability/instrument.js +6 -4
  123. package/dist/observability/instrument.js.map +1 -1
  124. package/dist/observability/sentry.d.ts.map +1 -1
  125. package/dist/observability/sentry.js +3 -2
  126. package/dist/observability/sentry.js.map +1 -1
  127. package/dist/runtime/conversation-runtime.js +13 -3
  128. package/dist/runtime/conversation-runtime.js.map +1 -1
  129. package/dist/runtime/types.d.ts +4 -0
  130. package/dist/runtime/types.d.ts.map +1 -1
  131. package/dist/runtime/types.js.map +1 -1
  132. package/dist/sandbox/cloudflare.d.ts +1 -0
  133. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  134. package/dist/sandbox/cloudflare.js +4 -1
  135. package/dist/sandbox/cloudflare.js.map +1 -1
  136. package/dist/sandbox/container.d.ts +1 -0
  137. package/dist/sandbox/container.d.ts.map +1 -1
  138. package/dist/sandbox/container.js +4 -1
  139. package/dist/sandbox/container.js.map +1 -1
  140. package/dist/sandbox/firecracker.d.ts +1 -0
  141. package/dist/sandbox/firecracker.d.ts.map +1 -1
  142. package/dist/sandbox/firecracker.js +4 -1
  143. package/dist/sandbox/firecracker.js.map +1 -1
  144. package/dist/sandbox/gondolin-bootstrap.d.ts +26 -0
  145. package/dist/sandbox/gondolin-bootstrap.d.ts.map +1 -0
  146. package/dist/sandbox/gondolin-bootstrap.js +27 -0
  147. package/dist/sandbox/gondolin-bootstrap.js.map +1 -0
  148. package/dist/sandbox/gondolin-contract.d.ts +125 -0
  149. package/dist/sandbox/gondolin-contract.d.ts.map +1 -0
  150. package/dist/sandbox/gondolin-contract.js +155 -0
  151. package/dist/sandbox/gondolin-contract.js.map +1 -0
  152. package/dist/sandbox/gondolin-fleet.d.ts +1 -1
  153. package/dist/sandbox/gondolin-fleet.d.ts.map +1 -1
  154. package/dist/sandbox/gondolin-fleet.js +1 -1
  155. package/dist/sandbox/gondolin-fleet.js.map +1 -1
  156. package/dist/sandbox/gondolin-gateway.d.ts +1 -0
  157. package/dist/sandbox/gondolin-gateway.d.ts.map +1 -1
  158. package/dist/sandbox/gondolin-gateway.js +13 -6
  159. package/dist/sandbox/gondolin-gateway.js.map +1 -1
  160. package/dist/sandbox/gondolin-inventory.d.ts +2 -16
  161. package/dist/sandbox/gondolin-inventory.d.ts.map +1 -1
  162. package/dist/sandbox/gondolin-inventory.js +2 -12
  163. package/dist/sandbox/gondolin-inventory.js.map +1 -1
  164. package/dist/sandbox/gondolin-placement.d.ts +0 -2
  165. package/dist/sandbox/gondolin-placement.d.ts.map +1 -1
  166. package/dist/sandbox/gondolin-placement.js +0 -6
  167. package/dist/sandbox/gondolin-placement.js.map +1 -1
  168. package/dist/sandbox/gondolin-recovery.d.ts +33 -0
  169. package/dist/sandbox/gondolin-recovery.d.ts.map +1 -0
  170. package/dist/sandbox/gondolin-recovery.js +39 -0
  171. package/dist/sandbox/gondolin-recovery.js.map +1 -0
  172. package/dist/sandbox/gondolin-remote.d.ts +2 -5
  173. package/dist/sandbox/gondolin-remote.d.ts.map +1 -1
  174. package/dist/sandbox/gondolin-remote.js +12 -39
  175. package/dist/sandbox/gondolin-remote.js.map +1 -1
  176. package/dist/sandbox/gondolin-worker-client.d.ts +8 -12
  177. package/dist/sandbox/gondolin-worker-client.d.ts.map +1 -1
  178. package/dist/sandbox/gondolin-worker-client.js +13 -15
  179. package/dist/sandbox/gondolin-worker-client.js.map +1 -1
  180. package/dist/sandbox/gondolin-worker.d.ts +2 -40
  181. package/dist/sandbox/gondolin-worker.d.ts.map +1 -1
  182. package/dist/sandbox/gondolin-worker.js.map +1 -1
  183. package/dist/sandbox/gondolin.d.ts +1 -0
  184. package/dist/sandbox/gondolin.d.ts.map +1 -1
  185. package/dist/sandbox/gondolin.js +8 -4
  186. package/dist/sandbox/gondolin.js.map +1 -1
  187. package/dist/sandbox/host.d.ts +1 -0
  188. package/dist/sandbox/host.d.ts.map +1 -1
  189. package/dist/sandbox/host.js +3 -0
  190. package/dist/sandbox/host.js.map +1 -1
  191. package/dist/sandbox/index.d.ts +9 -1
  192. package/dist/sandbox/index.d.ts.map +1 -1
  193. package/dist/sandbox/index.js +14 -0
  194. package/dist/sandbox/index.js.map +1 -1
  195. package/dist/sandbox/types.d.ts +5 -0
  196. package/dist/sandbox/types.d.ts.map +1 -1
  197. package/dist/sandbox/types.js.map +1 -1
  198. package/dist/sandbox/utils.d.ts +1 -0
  199. package/dist/sandbox/utils.d.ts.map +1 -1
  200. package/dist/sandbox/utils.js +5 -2
  201. package/dist/sandbox/utils.js.map +1 -1
  202. package/dist/sessions/chat-history-sync.d.ts +32 -0
  203. package/dist/sessions/chat-history-sync.d.ts.map +1 -0
  204. package/dist/sessions/{agent-memory-file-manager.js → chat-history-sync.js} +24 -83
  205. package/dist/sessions/chat-history-sync.js.map +1 -0
  206. package/dist/sessions/conversation-log.d.ts +13 -0
  207. package/dist/sessions/conversation-log.d.ts.map +1 -0
  208. package/dist/sessions/conversation-log.js +47 -0
  209. package/dist/sessions/conversation-log.js.map +1 -0
  210. package/dist/sessions/rotation.d.ts +2 -0
  211. package/dist/sessions/rotation.d.ts.map +1 -0
  212. package/dist/sessions/rotation.js +39 -0
  213. package/dist/sessions/rotation.js.map +1 -0
  214. package/dist/sessions/store.d.ts.map +1 -1
  215. package/dist/sessions/store.js +4 -10
  216. package/dist/sessions/store.js.map +1 -1
  217. package/dist/sessions/types.d.ts +2 -2
  218. package/dist/sessions/types.d.ts.map +1 -1
  219. package/dist/sessions/types.js.map +1 -1
  220. package/dist/settings-mutation.d.ts +23 -0
  221. package/dist/settings-mutation.d.ts.map +1 -0
  222. package/dist/settings-mutation.js +42 -0
  223. package/dist/settings-mutation.js.map +1 -0
  224. package/dist/tools/event.d.ts.map +1 -1
  225. package/dist/tools/event.js +27 -46
  226. package/dist/tools/event.js.map +1 -1
  227. package/dist/tools/host-fn-tool.d.ts +21 -0
  228. package/dist/tools/host-fn-tool.d.ts.map +1 -0
  229. package/dist/tools/host-fn-tool.js +32 -0
  230. package/dist/tools/host-fn-tool.js.map +1 -0
  231. package/dist/tools/index.d.ts +1 -0
  232. package/dist/tools/index.d.ts.map +1 -1
  233. package/dist/tools/index.js +1 -0
  234. package/dist/tools/index.js.map +1 -1
  235. package/dist/tools/react.d.ts.map +1 -1
  236. package/dist/tools/react.js +6 -17
  237. package/dist/tools/react.js.map +1 -1
  238. package/dist/tools/read.d.ts.map +1 -1
  239. package/dist/tools/read.js +7 -26
  240. package/dist/tools/read.js.map +1 -1
  241. package/dist/tools/sandbox.d.ts.map +1 -1
  242. package/dist/tools/sandbox.js +5 -2
  243. package/dist/tools/sandbox.js.map +1 -1
  244. package/dist/tools/subagent-slots.d.ts +28 -0
  245. package/dist/tools/subagent-slots.d.ts.map +1 -0
  246. package/dist/tools/subagent-slots.js +57 -0
  247. package/dist/tools/subagent-slots.js.map +1 -0
  248. package/dist/tools/subagent.d.ts +7 -2
  249. package/dist/tools/subagent.d.ts.map +1 -1
  250. package/dist/tools/subagent.js +160 -111
  251. package/dist/tools/subagent.js.map +1 -1
  252. package/dist/tools/types.d.ts +10 -9
  253. package/dist/tools/types.d.ts.map +1 -1
  254. package/dist/tools/types.js.map +1 -1
  255. package/dist/types.d.ts +5 -24
  256. package/dist/types.d.ts.map +1 -1
  257. package/dist/types.js.map +1 -1
  258. package/dist/utils/date.d.ts +1 -1
  259. package/dist/utils/date.js +1 -1
  260. package/dist/utils/date.js.map +1 -1
  261. package/dist/vault/disabled.d.ts.map +1 -1
  262. package/dist/vault/disabled.js +0 -1
  263. package/dist/vault/disabled.js.map +1 -1
  264. package/dist/vault/index.d.ts +0 -2
  265. package/dist/vault/index.d.ts.map +1 -1
  266. package/dist/vault/index.js +0 -10
  267. package/dist/vault/index.js.map +1 -1
  268. package/dist/vault/injection.d.ts +20 -0
  269. package/dist/vault/injection.d.ts.map +1 -0
  270. package/dist/vault/injection.js +27 -0
  271. package/dist/vault/injection.js.map +1 -0
  272. package/dist/vault/types.d.ts +0 -2
  273. package/dist/vault/types.d.ts.map +1 -1
  274. package/dist/vault/types.js.map +1 -1
  275. package/dist/web/admin/portal.d.ts +3 -3
  276. package/dist/web/admin/portal.d.ts.map +1 -1
  277. package/dist/web/admin/portal.js +41 -39
  278. package/dist/web/admin/portal.js.map +1 -1
  279. package/dist/web/admin/types.d.ts +3 -0
  280. package/dist/web/admin/types.d.ts.map +1 -1
  281. package/dist/web/admin/types.js.map +1 -1
  282. package/dist/web/agent-events/portal.d.ts.map +1 -1
  283. package/dist/web/agent-events/portal.js +2 -1
  284. package/dist/web/agent-events/portal.js.map +1 -1
  285. package/package.json +1 -1
  286. package/dist/sessions/agent-memory-file-manager.d.ts +0 -19
  287. package/dist/sessions/agent-memory-file-manager.d.ts.map +0 -1
  288. package/dist/sessions/agent-memory-file-manager.js.map +0 -1
  289. package/dist/vault/routing.d.ts +0 -4
  290. package/dist/vault/routing.d.ts.map +0 -1
  291. package/dist/vault/routing.js +0 -6
  292. package/dist/vault/routing.js.map +0 -1
@@ -5,12 +5,12 @@ import * as log from "../../log.js";
5
5
  import { ensureDirExists, readJsonSchemaFileIfExists } from "../../utils/file-guards.js";
6
6
  import { atomicWritePrivateFile } from "../../utils/fs-atomic.js";
7
7
  import { resolveChatSessionKey } from "../../sessions/policy.js";
8
- import { appendBotResponseLog, appendChannelLog, MessagingEventQueue, splitText, withRetry, } from "../shared.js";
8
+ import { appendBotResponseLog, appendChannelLog, MessagingEventQueue, splitText, } from "../shared.js";
9
9
  import { processMessageIntake } from "../intake.js";
10
- import { GithubApiError, GithubClient, githubIsRateLimited } from "./client.js";
11
- import { CLOUD_BUILD_APP_SLUG, CloudBuildLogUnavailableError, fetchCloudBuildLog, isCloudBuildId, projectFromDetailsUrl, } from "./cloudbuild.js";
10
+ import { GithubClient, GITHUB_MAX_COMMENT_LENGTH, githubRetry } from "./client.js";
12
11
  import { createGithubAdapters } from "./context.js";
13
- import { cloneRepo, GITHUB_PUSH_BRANCH_PATTERN, pushBranch, syncRepo } from "./repo.js";
12
+ import { fetchIsPr, fetchPrHeadBranch, GithubOps } from "./github-ops.js";
13
+ import { cloneRepo, conversationRepoDir } from "./repo.js";
14
14
  import { buildGithubConversationId, GITHUB_ISSUE_BODY_TS, githubReviewCommentTs, parseGithubConversationId, parseReviewCommentTs, } from "./ids.js";
15
15
  const SyncStateSchema = Type.Object({
16
16
  repos: Type.Record(Type.String(), Type.Object({
@@ -47,9 +47,6 @@ const PERMISSION_RANK = {
47
47
  const REQUIRED_TRIGGER_RANK = PERMISSION_RANK.write;
48
48
  /** Permission lookups are cached this long per repo+user. */
49
49
  const PERMISSION_CACHE_TTL_MS = 5 * 60 * 1000;
50
- const githubRetry = (fn) => withRetry(fn, { isRateLimited: githubIsRateLimited });
51
- /** GitHub comment bodies cap at 65536 chars; leave headroom for markup. */
52
- export const GITHUB_MAX_COMMENT_LENGTH = 60000;
53
50
  export const formatGithubContinuation = (partNum) => `*(continued ${partNum})*`;
54
51
  /** Slack-style emoji short names → GitHub reaction content values. */
55
52
  const GITHUB_REACTIONS = {
@@ -117,8 +114,6 @@ export class GithubMessagingBot {
117
114
  this.queues = new Map();
118
115
  this.repoState = new Map();
119
116
  this.permissionCache = new Map();
120
- /** Cloud Build ids seen in getChecks summaries → their GCP project. */
121
- this.buildRef = new Map();
122
117
  this.pollInFlight = false;
123
118
  this.handler = handler;
124
119
  this.config = config;
@@ -129,6 +124,10 @@ export class GithubMessagingBot {
129
124
  privateKey: config.privateKey,
130
125
  installationId: config.installationId,
131
126
  });
127
+ this.ops = new GithubOps(this.client, {
128
+ workingDir: config.workingDir,
129
+ cloudBuild: config.cloudBuild,
130
+ });
132
131
  }
133
132
  // ==========================================================================
134
133
  // Public API (implements MessagingBot)
@@ -301,65 +300,30 @@ export class GithubMessagingBot {
301
300
  // list mostly echoes the comments list; only issues *created* since the
302
301
  // baseline are new conversations (their body is the first message).
303
302
  const issues = (await this.client.listIssuesSince(repo.owner, repo.repo, since)) ?? [];
304
- for (const issue of issues) {
305
- if (issue.updated_at > state.cursor) {
306
- state.cursor = issue.updated_at;
307
- changed = true;
308
- }
309
- // Edits re-surface old items with a fresh updated_at; the id watermark
310
- // plus the creation baseline decide what actually triggers.
311
- if (issue.created_at < state.baseline || state.seenIssues.has(issue.id))
312
- continue;
313
- state.seenIssues.add(issue.id);
314
- changed = true;
315
- if (issue.user.type === "Bot")
316
- continue;
317
- await this.handleIncoming({
303
+ changed =
304
+ (await this.advanceFeed(state, state.seenIssues, issues, (issue) => ({
318
305
  ref: { ...repo, number: issue.number },
319
306
  ts: GITHUB_ISSUE_BODY_TS,
320
307
  user: issue.user.login,
321
308
  text: `# ${issue.title}\n\n${issue.body ?? ""}`.trim(),
322
309
  createdAt: issue.created_at,
323
310
  isPr: Boolean(issue.pull_request),
324
- });
325
- }
311
+ }))) || changed;
326
312
  const comments = (await this.client.listIssueCommentsSince(repo.owner, repo.repo, since)) ?? [];
327
- for (const comment of comments) {
328
- if (comment.updated_at > state.cursor) {
329
- state.cursor = comment.updated_at;
330
- changed = true;
331
- }
332
- if (comment.created_at < state.baseline || state.seenComments.has(comment.id))
333
- continue;
334
- state.seenComments.add(comment.id);
335
- changed = true;
336
- if (comment.user.type === "Bot")
337
- continue;
338
- await this.handleIncoming({
313
+ changed =
314
+ (await this.advanceFeed(state, state.seenComments, comments, (comment) => ({
339
315
  ref: { ...repo, number: issueNumberFromUrl(comment.issue_url) },
340
316
  ts: String(comment.id),
341
317
  user: comment.user.login,
342
318
  text: comment.body,
343
319
  createdAt: comment.created_at,
344
- });
345
- }
320
+ }))) || changed;
346
321
  // Inline PR review comments live in their own endpoint and id space; the
347
322
  // same watermark discipline applies. The bot's own thread replies (the
348
323
  // github_review_reply tool) re-surface here and are dropped as Bot-authored.
349
324
  const reviewComments = (await this.client.listPullReviewCommentsSince(repo.owner, repo.repo, since)) ?? [];
350
- for (const comment of reviewComments) {
351
- if (comment.updated_at > state.cursor) {
352
- state.cursor = comment.updated_at;
353
- changed = true;
354
- }
355
- if (comment.created_at < state.baseline || state.seenReviewComments.has(comment.id)) {
356
- continue;
357
- }
358
- state.seenReviewComments.add(comment.id);
359
- changed = true;
360
- if (comment.user.type === "Bot")
361
- continue;
362
- await this.handleIncoming({
325
+ changed =
326
+ (await this.advanceFeed(state, state.seenReviewComments, reviewComments, (comment) => ({
363
327
  ref: { ...repo, number: prNumberFromUrl(comment.pull_request_url) },
364
328
  ts: githubReviewCommentTs(comment.id),
365
329
  user: comment.user.login,
@@ -373,13 +337,36 @@ export class GithubMessagingBot {
373
337
  diffHunk: comment.diff_hunk,
374
338
  inReplyToId: comment.in_reply_to_id,
375
339
  },
376
- });
377
- }
340
+ }))) || changed;
378
341
  pruneSeen(state.seenComments);
379
342
  pruneSeen(state.seenIssues);
380
343
  pruneSeen(state.seenReviewComments);
381
344
  return changed;
382
345
  }
346
+ /**
347
+ * One feed's watermark advance — the same discipline for all three feeds:
348
+ * bump the cursor on any fresher updated_at, then let the id watermark plus
349
+ * the creation baseline decide what actually triggers (edits re-surface old
350
+ * items with a fresh updated_at). Bot-authored items advance the watermark
351
+ * without triggering.
352
+ */
353
+ async advanceFeed(state, seen, items, toIncoming) {
354
+ let changed = false;
355
+ for (const item of items) {
356
+ if (item.updated_at > state.cursor) {
357
+ state.cursor = item.updated_at;
358
+ changed = true;
359
+ }
360
+ if (item.created_at < state.baseline || seen.has(item.id))
361
+ continue;
362
+ seen.add(item.id);
363
+ changed = true;
364
+ if (item.user.type === "Bot")
365
+ continue;
366
+ await this.handleIncoming(toIncoming(item));
367
+ }
368
+ return changed;
369
+ }
383
370
  // ==========================================================================
384
371
  // Private - watermark persistence
385
372
  // ==========================================================================
@@ -394,7 +381,8 @@ export class GithubMessagingBot {
394
381
  return undefined;
395
382
  }
396
383
  }
397
- persistSyncState() {
384
+ /** The persisted form of the current watermarks; the test/inspection surface. */
385
+ syncStateSnapshot() {
398
386
  const state = { repos: {} };
399
387
  for (const [repoKey, watermark] of this.repoState) {
400
388
  state.repos[repoKey] = {
@@ -405,8 +393,11 @@ export class GithubMessagingBot {
405
393
  seenReviewComments: [...watermark.seenReviewComments],
406
394
  };
407
395
  }
396
+ return state;
397
+ }
398
+ persistSyncState() {
408
399
  ensureDirExists(dirname(this.config.syncStatePath));
409
- atomicWritePrivateFile(this.config.syncStatePath, JSON.stringify(state));
400
+ atomicWritePrivateFile(this.config.syncStatePath, JSON.stringify(this.syncStateSnapshot()));
410
401
  }
411
402
  // ==========================================================================
412
403
  // Private - triggering
@@ -535,8 +526,8 @@ export class GithubMessagingBot {
535
526
  if (issue && isPrHint === undefined)
536
527
  isPrHint = Boolean(issue.pull_request);
537
528
  }
538
- if (!existsSync(this.repoDir(conversationId))) {
539
- const isPr = isPrHint ?? (await this.fetchIsPr(item.ref));
529
+ if (!existsSync(conversationRepoDir(this.config.workingDir, conversationId))) {
530
+ const isPr = isPrHint ?? (await fetchIsPr(this.client, item.ref));
540
531
  await this.ensureRepoClone(item.ref, conversationId, isPr);
541
532
  }
542
533
  }
@@ -614,53 +605,22 @@ export class GithubMessagingBot {
614
605
  return issue;
615
606
  }
616
607
  // ==========================================================================
617
- // Repo clone + pull requests
608
+ // Repo clone
618
609
  // ==========================================================================
619
- repoDir(conversationId) {
620
- return join(this.config.workingDir, conversationId, "repo");
621
- }
622
- /** Whether the conversation's issue is a PR, for clone retries where the
623
- * triggering item didn't carry that knowledge. Errs toward plain issue. */
624
- async fetchIsPr(ref) {
625
- try {
626
- const issue = await githubRetry(() => this.client.getIssue(ref.owner, ref.repo, ref.number));
627
- return Boolean(issue.pull_request);
628
- }
629
- catch {
630
- return false;
631
- }
632
- }
633
- /**
634
- * The PR's head branch name when it lives in this repo — that is the name
635
- * the local checkout should carry, so committing on it and calling
636
- * github_pr pushes back to the PR itself instead of opening a new one.
637
- * Fork heads (unpushable with the repo-scoped token) and failed lookups
638
- * return undefined; callers then fall back to the synthetic pr-<n>.
639
- */
640
- async fetchPrHeadBranch(ref) {
641
- try {
642
- const pr = await githubRetry(() => this.client.getPullRequest(ref.owner, ref.repo, ref.number));
643
- const sameRepo = pr.head?.repo?.full_name?.toLowerCase() === `${ref.owner}/${ref.repo}`.toLowerCase();
644
- return sameRepo ? pr.head?.ref : undefined;
645
- }
646
- catch {
647
- return undefined;
648
- }
649
- }
650
610
  /**
651
611
  * Clone the conversation's repo into its dir (bind-mounted into the
652
612
  * sandbox) using an ephemeral contents:read token that never leaves the
653
613
  * host. Non-fatal: a failed clone degrades to the pre-clone experience.
654
614
  */
655
615
  async ensureRepoClone(ref, conversationId, isPr) {
656
- const dir = this.repoDir(conversationId);
616
+ const dir = conversationRepoDir(this.config.workingDir, conversationId);
657
617
  if (existsSync(dir))
658
618
  return;
659
619
  try {
660
620
  const token = await this.client.createScopedInstallationToken(ref.repo, {
661
621
  contents: "read",
662
622
  });
663
- const prHeadBranch = isPr ? await this.fetchPrHeadBranch(ref) : undefined;
623
+ const prHeadBranch = isPr ? await fetchPrHeadBranch(this.client, ref) : undefined;
664
624
  await cloneRepo({
665
625
  url: `https://github.com/${ref.owner}/${ref.repo}.git`,
666
626
  dir,
@@ -676,356 +636,5 @@ export class GithubMessagingBot {
676
636
  log.logWarning(`GitHub: repo clone failed for ${conversationId}`, err instanceof Error ? err.message : String(err));
677
637
  }
678
638
  }
679
- /**
680
- * Backs the `github_pr` tool: push a `pi/*` branch the agent prepared in
681
- * the conversation's clone, then open a pull request as the App. The write
682
- * token is minted per call, scoped to this one repo, and never enters the
683
- * sandbox.
684
- */
685
- async pushAndCreatePr(conversationId, request) {
686
- const ref = parseGithubConversationId(conversationId);
687
- const dir = this.repoDir(conversationId);
688
- if (!existsSync(dir)) {
689
- throw new Error("This conversation has no ./repo clone to push from.");
690
- }
691
- if (!GITHUB_PUSH_BRANCH_PATTERN.test(request.branch)) {
692
- throw new Error(`Branch '${request.branch}' is not pushable: name it pi/<something> (e.g. pi/fix-${ref.number}).`);
693
- }
694
- const repository = await githubRetry(() => this.client.getRepository(ref.owner, ref.repo));
695
- const base = request.base ?? repository.default_branch;
696
- const token = await this.client.createScopedInstallationToken(ref.repo, {
697
- contents: "write",
698
- pull_requests: "write",
699
- });
700
- await pushBranch({ dir, branch: request.branch, token });
701
- try {
702
- const pr = await githubRetry(() => this.client.createPullRequest(ref.owner, ref.repo, {
703
- title: request.title,
704
- head: request.branch,
705
- base,
706
- body: request.body,
707
- draft: request.draft,
708
- }));
709
- log.logInfo(`[${conversationId}] Opened PR #${pr.number}: ${pr.html_url}`);
710
- return { number: pr.number, url: pr.html_url };
711
- }
712
- catch (err) {
713
- // Idempotency: pushing more commits to a branch that already has an
714
- // open PR updates that PR; the create call then 422s. Surface the
715
- // existing PR instead of failing the fix-CI-push-again loop.
716
- if (err instanceof GithubApiError &&
717
- err.status === 422 &&
718
- /already exists/i.test(err.message)) {
719
- const existing = await this.client.findOpenPullRequestByBranch(ref.owner, ref.repo, request.branch);
720
- if (existing) {
721
- log.logInfo(`[${conversationId}] Pushed to existing PR #${existing.number}: ${existing.html_url}`);
722
- return { number: existing.number, url: existing.html_url, updatedExisting: true };
723
- }
724
- }
725
- throw err;
726
- }
727
- }
728
- /**
729
- * Backs the `github_checks` tool: CI check runs for a branch the agent
730
- * pushed, or for the conversation's PR head when no branch is given.
731
- */
732
- async getChecks(conversationId, branch) {
733
- const ref = parseGithubConversationId(conversationId);
734
- let target = branch;
735
- if (!target) {
736
- let pr;
737
- try {
738
- pr = await githubRetry(() => this.client.getPullRequest(ref.owner, ref.repo, ref.number));
739
- }
740
- catch {
741
- pr = null;
742
- }
743
- if (!pr?.head) {
744
- throw new Error("This conversation is not a pull request — pass the branch whose checks you want.");
745
- }
746
- target = pr.head.sha;
747
- }
748
- const runs = await githubRetry(() => this.client.listCheckRuns(ref.owner, ref.repo, target));
749
- return runs.map((run) => {
750
- const externalId = run.external_id?.trim() || null;
751
- let buildLogAvailable = false;
752
- // Cloud Build runs carry their build UUID in external_id and the GCP
753
- // project in details_url. Remember project-by-build so getBuildLog only
754
- // serves builds the agent actually saw on this repo's checks.
755
- if (this.config.cloudBuild &&
756
- run.app?.slug === CLOUD_BUILD_APP_SLUG &&
757
- externalId &&
758
- isCloudBuildId(externalId)) {
759
- const project = projectFromDetailsUrl(run.details_url) ?? this.config.cloudBuild.projectFallback;
760
- if (project) {
761
- this.rememberBuild(externalId, project);
762
- buildLogAvailable = true;
763
- }
764
- }
765
- return {
766
- id: run.id,
767
- name: run.name,
768
- status: run.status,
769
- conclusion: run.conclusion,
770
- url: run.html_url,
771
- appSlug: run.app?.slug ?? null,
772
- outputSummary: run.output?.summary?.trim() ? run.output.summary.slice(0, 500) : null,
773
- externalId,
774
- buildLogAvailable,
775
- };
776
- });
777
- }
778
- rememberBuild(buildId, project) {
779
- // Insertion-ordered Map as a small LRU: re-insert on touch, prune oldest.
780
- this.buildRef.delete(buildId);
781
- this.buildRef.set(buildId, { project });
782
- if (this.buildRef.size > 200) {
783
- const oldest = this.buildRef.keys().next().value;
784
- if (oldest !== undefined)
785
- this.buildRef.delete(oldest);
786
- }
787
- }
788
- /**
789
- * Backs the Cloud Build log mode of `github_checks`. GCP credentials stay
790
- * host-side (ADC via GOOGLE_APPLICATION_CREDENTIALS — e.g. a Workload
791
- * Identity Federation external_account file); the build must have appeared
792
- * in a github_checks summary first, which is also where its project is
793
- * learned. Unreadable logs degrade to the console url, never a dead end.
794
- */
795
- async getBuildLog(conversationId, buildId) {
796
- if (!this.config.cloudBuild) {
797
- throw new Error("Cloud Build log access is not configured on this host (no GCP credentials).");
798
- }
799
- const known = this.buildRef.get(buildId);
800
- if (!known) {
801
- throw new Error(`Unknown build id ${buildId}: run github_checks first and take a [build …] id from ` +
802
- `the summary.`);
803
- }
804
- let logText;
805
- try {
806
- logText = await fetchCloudBuildLog({
807
- tokenProvider: this.config.cloudBuild.tokenProvider,
808
- project: known.project,
809
- buildId,
810
- });
811
- }
812
- catch (err) {
813
- if (err instanceof CloudBuildLogUnavailableError) {
814
- throw new Error(`${err.message}${err.consoleUrl ? ` Console: ${err.consoleUrl}` : ""} Use the check's ` +
815
- `summary/url from github_checks, or reproduce the failure locally in ./repo.`, { cause: err });
816
- }
817
- throw err;
818
- }
819
- const MAX_LOG_CHARS = 20000;
820
- return logText.length > MAX_LOG_CHARS
821
- ? `…(truncated to the last ${MAX_LOG_CHARS} chars)\n${logText.slice(-MAX_LOG_CHARS)}`
822
- : logText;
823
- }
824
- /**
825
- * Backs the `github_sync` tool: refresh the conversation's ./repo clone
826
- * from origin with an ephemeral read token. Fetch-only when moving the
827
- * checkout could lose agent work; the formatted report tells the agent
828
- * what happened and what to do next.
829
- */
830
- async syncRepo(conversationId, branch) {
831
- const ref = parseGithubConversationId(conversationId);
832
- const dir = this.repoDir(conversationId);
833
- if (!existsSync(dir)) {
834
- throw new Error("This conversation has no ./repo clone to sync.");
835
- }
836
- const token = await this.client.createScopedInstallationToken(ref.repo, {
837
- contents: "read",
838
- });
839
- let prNumber;
840
- let prHeadBranch;
841
- let defaultBranch;
842
- if (!branch) {
843
- if (await this.fetchIsPr(ref)) {
844
- prNumber = ref.number;
845
- prHeadBranch = await this.fetchPrHeadBranch(ref);
846
- }
847
- else {
848
- const repository = await githubRetry(() => this.client.getRepository(ref.owner, ref.repo));
849
- defaultBranch = repository.default_branch;
850
- }
851
- }
852
- const result = await syncRepo({ dir, token, branch, prNumber, prHeadBranch, defaultBranch });
853
- if (result.updatedCheckout) {
854
- return `Updated ./repo: branch ${result.target} is now at ${result.fetchedSha.slice(0, 12)}.`;
855
- }
856
- const reasons = [];
857
- if (result.currentBranch !== result.target) {
858
- reasons.push(`the checkout is on '${result.currentBranch}', not '${result.target}'`);
859
- }
860
- if (result.dirty) {
861
- reasons.push("the working tree has uncommitted changes");
862
- }
863
- if (result.localCommits > 0) {
864
- reasons.push(`'${result.target}' has ${result.localCommits} local commit(s) not on origin`);
865
- }
866
- return (`Fetched ${result.target} to FETCH_HEAD (${result.fetchedSha.slice(0, 12)}) but left the ` +
867
- `checkout alone: ${reasons.join("; ")}. Merge or rebase FETCH_HEAD yourself ` +
868
- `(e.g. git merge FETCH_HEAD), or commit your work to a pi/* branch first.`);
869
- }
870
- /**
871
- * Backs the `github_read` tool: PR/issue metadata the ./repo clone cannot
872
- * provide (diff stats, review state, other issues in the repo). Owner/repo
873
- * always come from the conversation id, so reads never leave this repo.
874
- */
875
- async readGithub(conversationId, request) {
876
- const ref = parseGithubConversationId(conversationId);
877
- const number = request.number ?? ref.number;
878
- switch (request.action) {
879
- case "pr": {
880
- const pr = await githubRetry(() => this.client.getPullRequest(ref.owner, ref.repo, number));
881
- return { kind: "pr", pr };
882
- }
883
- case "pr_files": {
884
- const files = await githubRetry(() => this.client.listPullRequestFiles(ref.owner, ref.repo, number));
885
- return { kind: "pr_files", files };
886
- }
887
- case "pr_reviews": {
888
- const [reviews, threads] = await Promise.all([
889
- githubRetry(() => this.client.listPullRequestReviews(ref.owner, ref.repo, number)),
890
- githubRetry(() => this.client.listPullReviewComments(ref.owner, ref.repo, number)),
891
- ]);
892
- return { kind: "pr_reviews", reviews, threads };
893
- }
894
- case "issue": {
895
- const issue = await githubRetry(() => this.client.getIssue(ref.owner, ref.repo, number));
896
- return { kind: "issue", issue };
897
- }
898
- case "comments": {
899
- const comments = await githubRetry(() => this.client.listIssueComments(ref.owner, ref.repo, number));
900
- return { kind: "comments", comments };
901
- }
902
- case "list": {
903
- const issues = await githubRetry(() => this.client.listIssues(ref.owner, ref.repo, {
904
- state: request.state,
905
- labels: request.labels,
906
- creator: request.creator,
907
- }));
908
- return { kind: "list", issues };
909
- }
910
- default:
911
- throw new Error(`Unknown github_read action: ${String(request.action)}`);
912
- }
913
- }
914
- /**
915
- * Backs the `github_issue` tool: labels, assignees, and open/closed state
916
- * on any issue of this conversation's repo (triage). Same-repo by
917
- * construction — owner/repo come from the conversation id; the action enum
918
- * is closed, so lock/delete/transfer cannot be expressed at all.
919
- */
920
- async manageIssue(conversationId, request) {
921
- const ref = parseGithubConversationId(conversationId);
922
- const number = request.number ?? ref.number;
923
- try {
924
- switch (request.action) {
925
- case "add_labels": {
926
- if (!request.labels?.length) {
927
- throw new Error("add_labels requires a non-empty labels array.");
928
- }
929
- await githubRetry(() => this.client.addIssueLabels(ref.owner, ref.repo, number, request.labels));
930
- return `Added label(s) ${request.labels.join(", ")} to #${number}.`;
931
- }
932
- case "remove_label": {
933
- if (!request.label) {
934
- throw new Error("remove_label requires a label name.");
935
- }
936
- await githubRetry(() => this.client.removeIssueLabel(ref.owner, ref.repo, number, request.label));
937
- return `Removed label ${request.label} from #${number}.`;
938
- }
939
- case "add_assignees": {
940
- if (!request.assignees?.length) {
941
- throw new Error("add_assignees requires a non-empty assignees array.");
942
- }
943
- await githubRetry(() => this.client.addIssueAssignees(ref.owner, ref.repo, number, request.assignees));
944
- return `Assigned ${request.assignees.map((login) => `@${login}`).join(", ")} to #${number}.`;
945
- }
946
- case "remove_assignees": {
947
- if (!request.assignees?.length) {
948
- throw new Error("remove_assignees requires a non-empty assignees array.");
949
- }
950
- await githubRetry(() => this.client.removeIssueAssignees(ref.owner, ref.repo, number, request.assignees));
951
- return `Unassigned ${request.assignees.map((login) => `@${login}`).join(", ")} from #${number}.`;
952
- }
953
- case "close": {
954
- await githubRetry(() => this.client.updateIssueState(ref.owner, ref.repo, number, "closed", request.state_reason));
955
- return `Closed #${number}${request.state_reason ? ` as ${request.state_reason}` : ""}.`;
956
- }
957
- case "reopen": {
958
- await githubRetry(() => this.client.updateIssueState(ref.owner, ref.repo, number, "open"));
959
- return `Reopened #${number}.`;
960
- }
961
- default:
962
- throw new Error(`Unknown github_issue action: ${String(request.action)}`);
963
- }
964
- }
965
- catch (err) {
966
- if (err instanceof GithubApiError && err.status === 404) {
967
- throw new Error(`Issue #${number} not found in ${ref.owner}/${ref.repo}.`, { cause: err });
968
- }
969
- throw err;
970
- }
971
- }
972
- /**
973
- * Backs the `github_review_reply` tool: answer inside one inline review
974
- * thread of this conversation's PR. The comment id comes from an
975
- * `[PR review comment rc-<id> …]` message; GitHub validates it belongs to
976
- * this PR (404 otherwise, translated into agent-actionable guidance).
977
- */
978
- async replyToReviewThread(conversationId, commentId, body) {
979
- if (!Number.isInteger(commentId) || commentId <= 0) {
980
- throw new Error("comment_id must be the numeric id from an [PR review comment rc-<id> …] message.");
981
- }
982
- const ref = parseGithubConversationId(conversationId);
983
- const text = body.length > GITHUB_MAX_COMMENT_LENGTH
984
- ? `${body.slice(0, GITHUB_MAX_COMMENT_LENGTH)}\n…(truncated)`
985
- : body;
986
- try {
987
- const reply = await githubRetry(() => this.client.replyToReviewComment(ref.owner, ref.repo, ref.number, commentId, text));
988
- return {
989
- url: `https://github.com/${ref.owner}/${ref.repo}/pull/${ref.number}#discussion_r${reply.id}`,
990
- };
991
- }
992
- catch (err) {
993
- if (err instanceof GithubApiError && err.status === 404) {
994
- throw new Error(`Comment ${commentId} is not a review comment on this PR. Take the id from an ` +
995
- `[PR review comment rc-<id> …] message in this conversation; do not guess ids.`, { cause: err });
996
- }
997
- throw err;
998
- }
999
- }
1000
- /**
1001
- * Backs the log mode of `github_checks`: the tail of one CI job's log so
1002
- * the agent can diagnose a failing check without sandbox credentials. Only
1003
- * GitHub Actions checks have logs on GitHub — external CI apps (e.g. Cloud
1004
- * Build) keep theirs on their own service, so a 404 here is translated
1005
- * into guidance instead of a dead end.
1006
- */
1007
- async getJobLog(conversationId, jobId) {
1008
- if (!Number.isInteger(jobId) || jobId <= 0) {
1009
- throw new Error("job_id must be a positive Actions job id taken from a [job …] entry in the github_checks summary.");
1010
- }
1011
- const ref = parseGithubConversationId(conversationId);
1012
- let logText;
1013
- try {
1014
- logText = await githubRetry(() => this.client.getJobLog(ref.owner, ref.repo, jobId));
1015
- }
1016
- catch (err) {
1017
- if (err instanceof GithubApiError && err.status === 404) {
1018
- throw new Error(`No GitHub Actions log for job ${jobId}. Logs are only available for checks reported ` +
1019
- `by github-actions; external CI (e.g. Cloud Build) keeps logs on its own service — ` +
1020
- `use that check's summary/url from github_checks, or reproduce the failure locally ` +
1021
- `in ./repo instead. Do not retry with other ids.`, { cause: err });
1022
- }
1023
- throw err;
1024
- }
1025
- const MAX_LOG_CHARS = 20000;
1026
- return logText.length > MAX_LOG_CHARS
1027
- ? `…(truncated to the last ${MAX_LOG_CHARS} chars)\n${logText.slice(-MAX_LOG_CHARS)}`
1028
- : logText;
1029
- }
1030
639
  }
1031
640
  //# sourceMappingURL=bot.js.map