@capybara-hq/kernel 0.1.0-rc.3 → 0.1.0-rc.4

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 (226) hide show
  1. package/dist/admin/admin.controller.d.ts +9 -4
  2. package/dist/admin/admin.controller.js +13 -4
  3. package/dist/admin/extensions.controller.d.ts +8 -1
  4. package/dist/admin/extensions.controller.js +22 -2
  5. package/dist/agents/agent-lookup.d.ts +3 -0
  6. package/dist/agents/agent-lookup.js +10 -0
  7. package/dist/agents/agent-profile.service.d.ts +29 -0
  8. package/dist/agents/agent-profile.service.js +117 -0
  9. package/dist/agents/agents.controller.d.ts +93 -3
  10. package/dist/agents/agents.controller.js +114 -4
  11. package/dist/agents/agents.module.js +6 -2
  12. package/dist/agents/agents.service.d.ts +13 -5
  13. package/dist/agents/agents.service.js +30 -7
  14. package/dist/agents/definition.d.ts +4 -0
  15. package/dist/agents/definition.js +10 -0
  16. package/dist/agents/instruction-files.d.ts +60 -0
  17. package/dist/agents/instruction-files.js +111 -0
  18. package/dist/agents/models.controller.d.ts +9 -3
  19. package/dist/agents/models.controller.js +19 -4
  20. package/dist/agents/profile.d.ts +96 -0
  21. package/dist/agents/profile.js +51 -0
  22. package/dist/agents/readiness.controller.d.ts +26 -0
  23. package/dist/agents/readiness.controller.js +56 -0
  24. package/dist/agents/templates.js +1 -1
  25. package/dist/background/background.controller.d.ts +38 -0
  26. package/dist/background/background.controller.js +87 -0
  27. package/dist/background/background.module.d.ts +10 -0
  28. package/dist/background/background.module.js +32 -0
  29. package/dist/background/finish.d.ts +43 -0
  30. package/dist/background/finish.js +94 -0
  31. package/dist/background/launch-lock.d.ts +14 -0
  32. package/dist/background/launch-lock.js +39 -0
  33. package/dist/background/launch.d.ts +36 -0
  34. package/dist/background/launch.js +70 -0
  35. package/dist/background/poll.job.d.ts +53 -0
  36. package/dist/background/poll.job.js +182 -0
  37. package/dist/background/render.d.ts +23 -0
  38. package/dist/background/render.js +48 -0
  39. package/dist/background/session.d.ts +37 -0
  40. package/dist/background/session.js +66 -0
  41. package/dist/background/status.d.ts +35 -0
  42. package/dist/background/status.js +57 -0
  43. package/dist/background/stop.d.ts +62 -0
  44. package/dist/background/stop.js +225 -0
  45. package/dist/background/store.d.ts +150 -0
  46. package/dist/background/store.js +351 -0
  47. package/dist/background/store.module.d.ts +6 -0
  48. package/dist/background/store.module.js +22 -0
  49. package/dist/background/tasks.service.d.ts +67 -0
  50. package/dist/background/tasks.service.js +186 -0
  51. package/dist/background/wake-rules.d.ts +57 -0
  52. package/dist/background/wake-rules.js +83 -0
  53. package/dist/background/wake.d.ts +28 -0
  54. package/dist/background/wake.js +57 -0
  55. package/dist/catalog/claude/agents.js +2 -0
  56. package/dist/catalog/marketplaces.service.d.ts +1 -1
  57. package/dist/catalog/plugin-import.js +1 -1
  58. package/dist/catalog/plugin-upgrade.js +1 -1
  59. package/dist/channels/cards.js +1 -1
  60. package/dist/channels/connections.service.d.ts +16 -1
  61. package/dist/channels/connections.service.js +38 -4
  62. package/dist/channels/threads.d.ts +5 -0
  63. package/dist/channels/threads.js +8 -0
  64. package/dist/config/schema.d.ts +18 -0
  65. package/dist/config/schema.js +102 -0
  66. package/dist/conversations/access.js +2 -0
  67. package/dist/conversations/attachments.service.d.ts +30 -0
  68. package/dist/conversations/attachments.service.js +130 -5
  69. package/dist/conversations/conversations.controller.d.ts +36 -5
  70. package/dist/conversations/conversations.controller.js +62 -12
  71. package/dist/conversations/conversations.module.js +3 -0
  72. package/dist/conversations/conversations.service.d.ts +44 -2
  73. package/dist/conversations/conversations.service.js +140 -12
  74. package/dist/conversations/event-log.d.ts +1 -1
  75. package/dist/conversations/event-log.js +10 -1
  76. package/dist/conversations/events.d.ts +58 -0
  77. package/dist/conversations/events.js +57 -2
  78. package/dist/conversations/queue.d.ts +3 -1
  79. package/dist/conversations/queue.js +13 -2
  80. package/dist/conversations/queue.service.js +3 -1
  81. package/dist/conversations/titles.d.ts +7 -0
  82. package/dist/conversations/titles.js +15 -0
  83. package/dist/db/kernel-schema.d.ts +58 -2
  84. package/dist/db/pool.d.ts +5 -0
  85. package/dist/db/pool.js +3 -0
  86. package/dist/errors.d.ts +8 -0
  87. package/dist/errors.js +8 -0
  88. package/dist/evals/run-replay.d.ts +5 -2
  89. package/dist/evals/run-replay.js +30 -7
  90. package/dist/evals/step-replay.js +12 -6
  91. package/dist/evals/trajectory.js +1 -1
  92. package/dist/extensions/admin-ops.js +2 -0
  93. package/dist/extensions/admin-view.d.ts +3 -1
  94. package/dist/extensions/admin-view.js +10 -2
  95. package/dist/extensions/blobs.js +1 -0
  96. package/dist/extensions/config-issues.d.ts +13 -1
  97. package/dist/extensions/config-issues.js +22 -5
  98. package/dist/extensions/config-json-schema.d.ts +43 -7
  99. package/dist/extensions/config-json-schema.js +124 -58
  100. package/dist/extensions/deployment-config-store.d.ts +6 -0
  101. package/dist/extensions/deployment-config.d.ts +50 -3
  102. package/dist/extensions/deployment-config.js +125 -28
  103. package/dist/extensions/display-name.d.ts +22 -0
  104. package/dist/extensions/display-name.js +77 -0
  105. package/dist/extensions/extensions.module.js +6 -0
  106. package/dist/extensions/host.d.ts +26 -1
  107. package/dist/extensions/host.js +82 -9
  108. package/dist/extensions/org-config.controller.d.ts +11 -3
  109. package/dist/extensions/org-config.controller.js +14 -6
  110. package/dist/extensions/org-config.d.ts +6 -0
  111. package/dist/extensions/org-config.js +12 -13
  112. package/dist/extensions/package.d.ts +14 -1
  113. package/dist/extensions/package.js +51 -2
  114. package/dist/extensions/permission-view.d.ts +3 -0
  115. package/dist/extensions/point-defaults.controller.d.ts +34 -0
  116. package/dist/extensions/point-defaults.controller.js +71 -0
  117. package/dist/extensions/point-defaults.d.ts +64 -0
  118. package/dist/extensions/point-defaults.js +206 -0
  119. package/dist/extensions/registry.d.ts +1 -1
  120. package/dist/extensions/registry.js +2 -0
  121. package/dist/http/api.module.js +8 -0
  122. package/dist/http/nest-logger.d.ts +6 -5
  123. package/dist/http/nest-logger.js +24 -10
  124. package/dist/identity/auth.controller.js +9 -5
  125. package/dist/identity/cookies.d.ts +20 -5
  126. package/dist/identity/cookies.js +44 -6
  127. package/dist/identity/me.controller.d.ts +5 -1
  128. package/dist/identity/me.controller.js +12 -3
  129. package/dist/identity/sessions.d.ts +12 -0
  130. package/dist/identity/sessions.js +45 -0
  131. package/dist/index.d.ts +1 -1
  132. package/dist/infra/config/kernel-config.service.js +6 -0
  133. package/dist/infra/database/database.module.js +1 -0
  134. package/dist/infra/events/kernel-events.d.ts +8 -0
  135. package/dist/infra/lifecycle/process-guards.js +4 -0
  136. package/dist/infra/logging/logging.module.d.ts +4 -2
  137. package/dist/infra/logging/logging.module.js +24 -6
  138. package/dist/infra/queue/redis-connection.d.ts +6 -0
  139. package/dist/infra/queue/redis-connection.js +7 -0
  140. package/dist/infra/redis/redis.module.js +6 -2
  141. package/dist/infra/tokens.d.ts +6 -1
  142. package/dist/infra/tokens.js +6 -1
  143. package/dist/log.d.ts +13 -1
  144. package/dist/log.js +93 -17
  145. package/dist/models/purposes.d.ts +23 -0
  146. package/dist/models/purposes.js +31 -10
  147. package/dist/orgs/offboarding.d.ts +3 -1
  148. package/dist/orgs/offboarding.js +26 -16
  149. package/dist/orgs/settings.service.d.ts +5 -0
  150. package/dist/orgs/settings.service.js +13 -2
  151. package/dist/roles/worker.module.js +6 -0
  152. package/dist/runtime/context/build.d.ts +9 -7
  153. package/dist/runtime/context/build.js +27 -19
  154. package/dist/runtime/context/claude-tools.d.ts +2 -1
  155. package/dist/runtime/context/claude-tools.js +5 -4
  156. package/dist/runtime/context/compaction.js +4 -0
  157. package/dist/runtime/langgraph-loop.js +3 -2
  158. package/dist/runtime/loop.d.ts +4 -3
  159. package/dist/runtime/maintenance.d.ts +4 -2
  160. package/dist/runtime/maintenance.js +17 -5
  161. package/dist/runtime/model-call.d.ts +17 -1
  162. package/dist/runtime/model-call.js +3 -2
  163. package/dist/runtime/providers.d.ts +6 -0
  164. package/dist/runtime/providers.js +8 -0
  165. package/dist/runtime/run-queue.d.ts +25 -2
  166. package/dist/runtime/run-queue.js +129 -33
  167. package/dist/runtime/run-worker.js +16 -3
  168. package/dist/runtime/runtime.module.js +8 -3
  169. package/dist/runtime/step-log.d.ts +6 -0
  170. package/dist/runtime/step-log.js +16 -2
  171. package/dist/runtime/subagents/definition.js +3 -1
  172. package/dist/runtime/usage-report.d.ts +2 -1
  173. package/dist/runtime/usage-report.js +2 -1
  174. package/dist/runtime/waits.d.ts +8 -2
  175. package/dist/runtime/waits.js +37 -11
  176. package/dist/sandbox/sandbox.module.js +6 -1
  177. package/dist/sandbox/sandbox.service.d.ts +61 -3
  178. package/dist/sandbox/sandbox.service.js +141 -7
  179. package/dist/tools/approvals.service.js +2 -0
  180. package/dist/tools/builtin-names.d.ts +20 -2
  181. package/dist/tools/builtin-names.js +29 -2
  182. package/dist/tools/builtins/builtin-tools.d.ts +15 -1
  183. package/dist/tools/builtins/builtin-tools.js +34 -2
  184. package/dist/tools/builtins/sandbox/bash.d.ts +1 -1
  185. package/dist/tools/builtins/sandbox/bash.js +230 -3
  186. package/dist/tools/builtins/sandbox/common.d.ts +27 -4
  187. package/dist/tools/builtins/sandbox/common.js +7 -3
  188. package/dist/tools/builtins/sandbox/deliver.js +1 -1
  189. package/dist/tools/builtins/sandbox/index.d.ts +2 -0
  190. package/dist/tools/builtins/sandbox/index.js +6 -0
  191. package/dist/tools/builtins/sandbox/task-output.d.ts +63 -0
  192. package/dist/tools/builtins/sandbox/task-output.js +304 -0
  193. package/dist/tools/builtins/sandbox/task-stop.d.ts +10 -0
  194. package/dist/tools/builtins/sandbox/task-stop.js +112 -0
  195. package/dist/tools/builtins/web/errors.d.ts +8 -0
  196. package/dist/tools/builtins/web/errors.js +53 -0
  197. package/dist/tools/builtins/web/extract.d.ts +62 -0
  198. package/dist/tools/builtins/web/extract.js +267 -0
  199. package/dist/tools/builtins/web/fetch.d.ts +55 -0
  200. package/dist/tools/builtins/web/fetch.js +174 -0
  201. package/dist/tools/builtins/web/index.d.ts +13 -0
  202. package/dist/tools/builtins/web/index.js +12 -0
  203. package/dist/tools/builtins/web/page-cache.d.ts +25 -0
  204. package/dist/tools/builtins/web/page-cache.js +77 -0
  205. package/dist/tools/builtins/web/search.d.ts +25 -0
  206. package/dist/tools/builtins/web/search.js +209 -0
  207. package/dist/tools/builtins/web/url.d.ts +17 -0
  208. package/dist/tools/builtins/web/url.js +43 -0
  209. package/dist/tools/oauth/flow-paths.d.ts +1 -1
  210. package/dist/tools/oauth/flow-paths.js +1 -1
  211. package/dist/tools/pipeline.js +15 -0
  212. package/dist/tools/registry.d.ts +22 -3
  213. package/dist/tools/registry.js +26 -11
  214. package/dist/tools/tools.module.js +12 -0
  215. package/dist/tools/validate.js +1 -1
  216. package/package.json +3 -3
  217. package/src/migrations/0064_account_last_org.sql +7 -0
  218. package/src/migrations/0065_conversation_archive.sql +8 -0
  219. package/src/migrations/0066_extension_display_name.sql +4 -0
  220. package/src/migrations/0067_agent_profiles.sql +20 -0
  221. package/src/migrations/0068_agent_avatar_uploads.sql +7 -0
  222. package/src/migrations/0069_attachments_agent_idx.sql +3 -0
  223. package/src/migrations/0070_background_tasks.sql +37 -0
  224. package/src/migrations/0071_background_tasks_reserve.sql +6 -0
  225. package/src/migrations/0072_background_tasks_stopping.sql +5 -0
  226. package/src/migrations/0073_org_extension_point_defaults.sql +11 -0
@@ -20,6 +20,7 @@ import { ApiTokenService, FutureIsoDate } from "./api-tokens.js";
20
20
  import { MAX_PASSWORD_LENGTH } from "./login.js";
21
21
  import { CurrentPrincipal, requireUser, SessionOnly } from "./principal.js";
22
22
  import { SessionService } from "./sessions.js";
23
+ import { SettingsService } from "./settings.js";
23
24
  /** The credential throttlers of a password change (per account, per client IP). */
24
25
  const CHANGE_PASSWORD_THROTTLERS = ["change-password", "change-password-ip"];
25
26
  const ChangePasswordSchema = z.object({
@@ -45,13 +46,15 @@ let MeController = class MeController {
45
46
  tokens;
46
47
  throttler;
47
48
  log;
48
- constructor(accounts, sessions, orgs, tokens, throttler, log) {
49
+ settings;
50
+ constructor(accounts, sessions, orgs, tokens, throttler, log, settings) {
49
51
  this.accounts = accounts;
50
52
  this.sessions = sessions;
51
53
  this.orgs = orgs;
52
54
  this.tokens = tokens;
53
55
  this.throttler = throttler;
54
56
  this.log = log;
57
+ this.settings = settings;
55
58
  }
56
59
  async me(principal) {
57
60
  const p = requireUser(principal);
@@ -62,7 +65,12 @@ let MeController = class MeController {
62
65
  name: o.name,
63
66
  role: o.role,
64
67
  }));
65
- return { account: p.account, via: p.via, currentOrgId, memberships };
68
+ // a session's org the account has since left reads as none (removal clears it, but a session
69
+ // created at that moment may still name it)
70
+ const current = memberships.some((m) => m.orgId === currentOrgId) ? currentOrgId : null;
71
+ const { orgCreation } = await this.settings.get();
72
+ const canCreateOrg = orgCreation === "anyone" || p.account.isEnterpriseAdmin;
73
+ return { account: p.account, via: p.via, currentOrgId: current, memberships, canCreateOrg };
66
74
  }
67
75
  /** 切换当前组织(仅 Web 会话);`null` 清空。非成员或已软删除的组织 → `org.not_found`。 */
68
76
  async setCurrentOrg(principal, body) {
@@ -160,6 +168,7 @@ MeController = __decorate([
160
168
  __param(2, Inject(OrgsService)),
161
169
  __param(3, Inject(ApiTokenService)),
162
170
  __param(4, Inject(CredentialThrottler)),
163
- __param(5, Inject(KERNEL_LOGGER))
171
+ __param(5, Inject(KERNEL_LOGGER)),
172
+ __param(6, Inject(SettingsService))
164
173
  ], MeController);
165
174
  export { MeController };
@@ -36,6 +36,18 @@ export declare class SessionService {
36
36
  userAgent?: string;
37
37
  ip?: string;
38
38
  }): Promise<IssuedSession>;
39
+ /**
40
+ * The org a new session starts in: the account's last chosen one while it is still a member of it
41
+ * (and the org is not deleted), else its only org when it has exactly one, else none (the web
42
+ * app then asks to choose or create one).
43
+ */
44
+ private startingOrg;
45
+ /**
46
+ * Read-only: of several refresh tokens (a cookie header carrying the legacy and the `Path=/`
47
+ * cookie), the first that is the current token of a live session, if any. Choosing before
48
+ * rotating keeps a stale token from tripping replay detection on a still valid session.
49
+ */
50
+ pickCurrentRefresh(candidates: readonly string[]): Promise<string | undefined>;
39
51
  refresh(refreshToken: string, meta: {
40
52
  userAgent?: string;
41
53
  ip?: string;
@@ -72,11 +72,49 @@ let SessionService = class SessionService {
72
72
  user_agent: meta.userAgent ?? null,
73
73
  ip: meta.ip ?? null,
74
74
  expires_at: sql `now() + make_interval(days => ${refreshTtlDays})`,
75
+ current_org_id: await this.startingOrg(accountId),
75
76
  })
76
77
  .returning(["expires_at"])
77
78
  .executeTakeFirstOrThrow();
78
79
  return this.issue(id, accountId, refreshToken, row.expires_at);
79
80
  }
81
+ /**
82
+ * The org a new session starts in: the account's last chosen one while it is still a member of it
83
+ * (and the org is not deleted), else its only org when it has exactly one, else none (the web
84
+ * app then asks to choose or create one).
85
+ */
86
+ async startingOrg(accountId) {
87
+ const orgs = await this.db
88
+ .selectFrom("memberships as m")
89
+ .innerJoin("orgs as o", "o.id", "m.org_id")
90
+ .innerJoin("accounts as a", "a.id", "m.account_id")
91
+ .select(["m.org_id as orgId", "a.last_org_id as lastOrgId"])
92
+ .where("m.account_id", "=", accountId)
93
+ .where("o.deleted_at", "is", null)
94
+ .execute();
95
+ const last = orgs.find((o) => o.orgId === o.lastOrgId);
96
+ if (last)
97
+ return last.orgId;
98
+ return orgs.length === 1 ? (orgs[0]?.orgId ?? null) : null;
99
+ }
100
+ /**
101
+ * Read-only: of several refresh tokens (a cookie header carrying the legacy and the `Path=/`
102
+ * cookie), the first that is the current token of a live session, if any. Choosing before
103
+ * rotating keeps a stale token from tripping replay detection on a still valid session.
104
+ */
105
+ async pickCurrentRefresh(candidates) {
106
+ if (candidates.length === 0)
107
+ return undefined;
108
+ const rows = await this.db
109
+ .selectFrom("sessions")
110
+ .select("refresh_hash")
111
+ .where("refresh_hash", "in", candidates.map((c) => sha256Hex(c)))
112
+ .where("revoked_at", "is", null)
113
+ .where("expires_at", ">", sql `now()`)
114
+ .execute();
115
+ const live = new Set(rows.map((r) => r.refresh_hash));
116
+ return candidates.find((c) => live.has(sha256Hex(c)));
117
+ }
80
118
  async refresh(refreshToken, meta) {
81
119
  const oldHash = sha256Hex(refreshToken);
82
120
  const next = newRefreshToken();
@@ -277,6 +315,13 @@ let SessionService = class SessionService {
277
315
  .executeTakeFirst());
278
316
  if (!m)
279
317
  throw new ApiError("org.not_found", "Organization not found");
318
+ // remembered for the next sign-in; the account row before the session row, the order a
319
+ // password change takes (account, then its sessions)
320
+ await this.db
321
+ .updateTable("accounts")
322
+ .set({ last_org_id: orgId })
323
+ .where("id", "=", accountId)
324
+ .execute();
280
325
  const r = await this.db
281
326
  .updateTable("sessions")
282
327
  .set({ current_org_id: orgId })
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export { type ApprovalDecision, ExtensionApprovals, type PendingExtension, type
31
31
  export { blobKey, ExtensionBlobIntegrityError, type ExtensionBlobSource, ExtensionBlobs, } from "./extensions/blobs.js";
32
32
  export { BOOTSTRAP_MANIFEST_FILE, type BootstrapEntryResult, BootstrapFailedError, type BootstrapFailure, type BootstrapResolveOptions, type BootstrapResult, BootstrapService, bootstrapManifestSha256, readBootstrapManifest, resolveBootstrapPackages, } from "./extensions/bootstrap.js";
33
33
  export { type ExtensionCli, openExtensionCli } from "./extensions/cli.js";
34
- export { DEPLOYMENT_SECRETS_ORG, type DeploymentConfigField, type DeploymentConfigView, ExtensionDeploymentConfigService, mergeDeploymentConfig, PLUGIN_DEPLOYMENT_CONFIG_TOPIC, readStoredDeploymentConfig, } from "./extensions/deployment-config.js";
34
+ export { DEPLOYMENT_SECRETS_ORG, type DeploymentConfigField, type DeploymentConfigView, ExtensionDeploymentConfigService, mergeDeploymentConfig, PLUGIN_DEPLOYMENT_CONFIG_TOPIC, readStoredDeploymentConfig, type SecretEntry, } from "./extensions/deployment-config.js";
35
35
  export { assertSecretsReachable, ExtensionHost, type ExtensionLoadPlan, type InspectedExtension, type InstalledExtensionLoader, type InstallFailureReason, } from "./extensions/host.js";
36
36
  export { ExtensionUnloadedError, type InflightEntry, InflightTracker, PLUGIN_UNLOADED_TOOL_ERROR, type TrackOptions, trackCalls, } from "./extensions/inflight.js";
37
37
  export { type DesiredExtension, type ExtensionInstallRow, ExtensionInstalls, type IngestSource, type PackageReview, } from "./extensions/installs.js";
@@ -34,6 +34,12 @@ const HOT_APPLIERS = {
34
34
  "subagents.maxConcurrentPerRun": () => { },
35
35
  "subagents.maxDispatchesPerRun": () => { },
36
36
  "subagents.maxTokensPerRun": () => { },
37
+ // The web tools read these limits through the config service on every call.
38
+ "web.search.maxResults": () => { },
39
+ "web.fetch.maxPageChars": () => { },
40
+ "web.fetch.maxAnswerTokens": () => { },
41
+ "web.fetch.cacheSeconds": () => { },
42
+ "web.fetch.timeoutMs": () => { },
37
43
  };
38
44
  const HOT_PATHS = configFields(KernelConfigSchema)
39
45
  .filter((f) => f.meta?.hot)
@@ -66,6 +66,7 @@ let DatabaseModule = class DatabaseModule {
66
66
  const { db } = config.get();
67
67
  return createDb(db.url, db.poolMax, (err) => log.error("pg idle client error", { err }), {
68
68
  connectTimeoutMs: db.connectTimeoutMs,
69
+ idleTimeoutMs: db.idleTimeoutMs,
69
70
  onClientError: (err) => log.warn("pg client error", { err }),
70
71
  });
71
72
  },
@@ -78,6 +78,14 @@ export interface KernelEventMap {
78
78
  "run.orphaned": {
79
79
  runId: string;
80
80
  };
81
+ /**
82
+ * A background task of the conversation finished (after the commit of its
83
+ * `background_task_finished` event). `BackgroundWake` asks the run queue for a wake run; a lost
84
+ * event is re-derived by the next `ensureRun` (a message, the end of a run, a page opening).
85
+ */
86
+ "background_task.finished": {
87
+ conversationId: string;
88
+ };
81
89
  }
82
90
  type Handler<K extends keyof KernelEventMap> = (payload: KernelEventMap[K]) => Promise<void> | void;
83
91
  /** Typed in-process event emitter; handlers run in order, a failing one is logged and skipped. */
@@ -39,6 +39,10 @@ export function createFatalRecorder(deps) {
39
39
  let recent = [];
40
40
  let open = false;
41
41
  return (kind, err) => {
42
+ // Exiting already: log nothing more. A failing log write (EPIPE on a dead stdout) raises the
43
+ // next event itself; logging it again would keep the process busy forever and starve the exit.
44
+ if (open)
45
+ return;
42
46
  try {
43
47
  const t = now();
44
48
  recent = recent.filter((x) => t - x < GUARD_CIRCUIT_WINDOW_MS);
@@ -6,8 +6,10 @@ export interface LoggingModuleOptions {
6
6
  logger?: KernelLogger;
7
7
  }
8
8
  /**
9
- * Provides the process logger as `KERNEL_LOGGER`. The logger is created before the Nest context
10
- * (it must exist for startup errors), so it is passed in or derived from the startup config here.
9
+ * Provides the process logger as `KERNEL_ROOT_LOGGER` and, as `KERNEL_LOGGER`, one child per injecting
10
+ * class with `context` = the class name (the `[context=…]` of the pretty output; octok's
11
+ * per-class Nest logger context). The logger is created before the Nest context (it must exist for
12
+ * startup errors), so it is passed in or derived from the startup config here.
11
13
  */
12
14
  export declare class LoggingModule {
13
15
  static forRoot(config: KernelConfig, opts?: LoggingModuleOptions): DynamicModule;
@@ -5,12 +5,15 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var LoggingModule_1;
8
- import { Global, Module } from "@nestjs/common";
8
+ import { Global, Module, Scope } from "@nestjs/common";
9
+ import { INQUIRER } from "@nestjs/core";
9
10
  import { createConfiguredLogger } from "../../log-config.js";
10
- import { KERNEL_LOGGER } from "../tokens.js";
11
+ import { KERNEL_LOGGER, KERNEL_ROOT_LOGGER } from "../tokens.js";
11
12
  /**
12
- * Provides the process logger as `KERNEL_LOGGER`. The logger is created before the Nest context
13
- * (it must exist for startup errors), so it is passed in or derived from the startup config here.
13
+ * Provides the process logger as `KERNEL_ROOT_LOGGER` and, as `KERNEL_LOGGER`, one child per injecting
14
+ * class with `context` = the class name (the `[context=…]` of the pretty output; octok's
15
+ * per-class Nest logger context). The logger is created before the Nest context (it must exist for
16
+ * startup errors), so it is passed in or derived from the startup config here.
14
17
  */
15
18
  let LoggingModule = LoggingModule_1 = class LoggingModule {
16
19
  static forRoot(config, opts = {}) {
@@ -21,8 +24,16 @@ let LoggingModule = LoggingModule_1 = class LoggingModule {
21
24
  return {
22
25
  module: LoggingModule_1,
23
26
  global: true,
24
- providers: [{ provide: KERNEL_LOGGER, useValue: logger }],
25
- exports: [KERNEL_LOGGER],
27
+ providers: [
28
+ { provide: KERNEL_ROOT_LOGGER, useValue: logger },
29
+ {
30
+ provide: KERNEL_LOGGER,
31
+ scope: Scope.TRANSIENT,
32
+ inject: [KERNEL_ROOT_LOGGER, INQUIRER],
33
+ useFactory: (root, inquirer) => contextLogger(root, inquirer),
34
+ },
35
+ ],
36
+ exports: [KERNEL_ROOT_LOGGER, KERNEL_LOGGER],
26
37
  };
27
38
  }
28
39
  };
@@ -32,3 +43,10 @@ LoggingModule = LoggingModule_1 = __decorate([
32
43
  // biome-ignore lint/complexity/noStaticOnlyClass: a Nest dynamic module
33
44
  ], LoggingModule);
34
45
  export { LoggingModule };
46
+ /** The child of `root` for the class instance `inquirer`; `root` itself for a factory provider. */
47
+ function contextLogger(root, inquirer) {
48
+ const name = inquirer?.constructor?.name;
49
+ return typeof name === "string" && name !== "" && name !== "Object"
50
+ ? root.child({ context: name })
51
+ : root;
52
+ }
@@ -1,4 +1,10 @@
1
1
  import type { RedisOptions } from "bullmq";
2
+ /**
3
+ * TCP keepalive of every long-lived kernel Redis connection: probes start after 10 s idle (the OS
4
+ * default is 2 hours), so a connection a dropped network left half-open is found and reconnected
5
+ * before a command waits on it, not at the next write (EPIPE).
6
+ */
7
+ export declare const REDIS_KEEPALIVE_MS = 10000;
2
8
  /**
3
9
  * BullMQ connection options from the kernel's Redis URL, the same connection the hand-made
4
10
  * `new Redis(url, { maxRetriesPerRequest: null })` workers use: BullMQ requires
@@ -1,3 +1,9 @@
1
+ /**
2
+ * TCP keepalive of every long-lived kernel Redis connection: probes start after 10 s idle (the OS
3
+ * default is 2 hours), so a connection a dropped network left half-open is found and reconnected
4
+ * before a command waits on it, not at the next write (EPIPE).
5
+ */
6
+ export const REDIS_KEEPALIVE_MS = 10_000;
1
7
  /**
2
8
  * BullMQ connection options from the kernel's Redis URL, the same connection the hand-made
3
9
  * `new Redis(url, { maxRetriesPerRequest: null })` workers use: BullMQ requires
@@ -14,5 +20,6 @@ export function bullConnectionOptions(url) {
14
20
  ...(db !== undefined && Number.isInteger(db) && { db }),
15
21
  ...(u.protocol === "rediss:" && { tls: {} }),
16
22
  maxRetriesPerRequest: null,
23
+ keepAlive: REDIS_KEEPALIVE_MS,
17
24
  };
18
25
  }
@@ -14,6 +14,7 @@ import { closeWithin } from "../close-within.js";
14
14
  import { KernelConfigService } from "../config/kernel-config.service.js";
15
15
  import { GracefulShutdownModule } from "../lifecycle/graceful-shutdown.module.js";
16
16
  import { GracefulShutdown } from "../lifecycle/graceful-shutdown.service.js";
17
+ import { REDIS_KEEPALIVE_MS } from "../queue/redis-connection.js";
17
18
  import { KERNEL_BUS, KERNEL_LOGGER, REDIS, REDIS_SUBSCRIBER } from "../tokens.js";
18
19
  /**
19
20
  * Closes the bus subscriber, then the main connection, at the end of the shutdown
@@ -64,7 +65,10 @@ RedisModule = __decorate([
64
65
  provide: REDIS,
65
66
  inject: [KernelConfigService, KERNEL_LOGGER],
66
67
  useFactory: (config, log) => {
67
- const redis = new Redis(config.get().redis.url, { lazyConnect: true });
68
+ const redis = new Redis(config.get().redis.url, {
69
+ lazyConnect: true,
70
+ keepAlive: REDIS_KEEPALIVE_MS,
71
+ });
68
72
  redis.on("error", (err) => log.error("redis connection error", { err }));
69
73
  return redis;
70
74
  },
@@ -72,7 +76,7 @@ RedisModule = __decorate([
72
76
  {
73
77
  provide: REDIS_SUBSCRIBER,
74
78
  inject: [KernelConfigService, KERNEL_LOGGER],
75
- useFactory: (config, log) => createBusSubscriber(() => new Redis(config.get().redis.url), log),
79
+ useFactory: (config, log) => createBusSubscriber(() => new Redis(config.get().redis.url, { keepAlive: REDIS_KEEPALIVE_MS }), log),
76
80
  },
77
81
  {
78
82
  provide: KERNEL_BUS,
@@ -1,6 +1,11 @@
1
1
  /** Injection tokens of the global infra modules. */
2
- /** The process logger (`KernelLogger`). */
2
+ /**
3
+ * A class's logger (`KernelLogger`): the process logger with `context` set to the injecting class's
4
+ * name. Transient, so `app.get` cannot resolve it; use `KERNEL_ROOT_LOGGER` outside an injection.
5
+ */
3
6
  export declare const KERNEL_LOGGER: unique symbol;
7
+ /** The process logger itself (`KernelLogger`), without a context. */
8
+ export declare const KERNEL_ROOT_LOGGER: unique symbol;
4
9
  /** The startup input of `KernelConfigService`: `{ config, env }`. */
5
10
  export declare const KERNEL_CONFIG_INPUT: unique symbol;
6
11
  /** The Kysely pool, untyped (`Kysely<any>`); infra only, services inject `KernelDb`. */
@@ -1,6 +1,11 @@
1
1
  /** Injection tokens of the global infra modules. */
2
- /** The process logger (`KernelLogger`). */
2
+ /**
3
+ * A class's logger (`KernelLogger`): the process logger with `context` set to the injecting class's
4
+ * name. Transient, so `app.get` cannot resolve it; use `KERNEL_ROOT_LOGGER` outside an injection.
5
+ */
3
6
  export const KERNEL_LOGGER = Symbol("KERNEL_LOGGER");
7
+ /** The process logger itself (`KernelLogger`), without a context. */
8
+ export const KERNEL_ROOT_LOGGER = Symbol("KERNEL_ROOT_LOGGER");
4
9
  /** The startup input of `KernelConfigService`: `{ config, env }`. */
5
10
  export const KERNEL_CONFIG_INPUT = Symbol("KERNEL_CONFIG_INPUT");
6
11
  /** The Kysely pool, untyped (`Kysely<any>`); infra only, services inject `KernelDb`. */
package/dist/log.d.ts CHANGED
@@ -30,6 +30,11 @@ export interface KernelLogger extends Logger {
30
30
  redact(text: string): string;
31
31
  /** Waits until buffered records reached every output (Loki); never rejects. Call before exit. */
32
32
  flush(): Promise<void>;
33
+ /**
34
+ * A logger that adds `fields` to every record (e.g. `{ context: "RunWorker" }`); call fields cannot
35
+ * overwrite them. It shares everything else with this logger: level, secrets, outputs.
36
+ */
37
+ child(fields: Record<string, string>): KernelLogger;
33
38
  }
34
39
  /**
35
40
  * 用与 logger 相同的匹配器打码任意文本(工具结果等不经过日志的内容用):展开规则同 `addOrgSecrets`
@@ -37,13 +42,15 @@ export interface KernelLogger extends Logger {
37
42
  * 不得小于匹配器的分桶前缀长度 4)。没有可用的值时原样返回。
38
43
  */
39
44
  export declare function createRedactor(values: readonly unknown[], minLength?: number): (s: string) => string;
40
- /** Console format: `pretty` (pino-pretty, one line per record), `json`, or `auto` (pretty on a TTY). */
45
+ /** Console format: `pretty` (pino-pretty), `json`, or `auto` (pretty on a TTY). */
41
46
  export declare const LOG_FORMATS: readonly ["auto", "pretty", "json"];
42
47
  /** Console format. */
43
48
  export type LogFormat = (typeof LOG_FORMATS)[number];
44
49
  /** A minimal writable the console output goes to (`process.stdout` by default). */
45
50
  export interface LogOutput {
46
51
  write(chunk: string): unknown;
52
+ /** A stream's `error` event (EPIPE when its reader is gone). */
53
+ on?(event: "error", listener: (err: unknown) => void): unknown;
47
54
  }
48
55
  /** Options of {@link createLogger}. */
49
56
  export interface LoggerOptions {
@@ -58,6 +65,11 @@ export interface LoggerOptions {
58
65
  out?: LogOutput;
59
66
  /** Whether the console is a terminal (picks `auto` and colors); default `process.stdout.isTTY`. */
60
67
  isTTY?: boolean;
68
+ /**
69
+ * Pretty output in colors; default: `FORCE_COLOR` (set and not `0`) turns them on, `NO_COLOR` off,
70
+ * else a TTY. `pnpm dev:kernel` forces them, since its output goes through `tee`.
71
+ */
72
+ colors?: boolean;
61
73
  /** Also push every record to Loki. */
62
74
  loki?: LokiOptions;
63
75
  /** Fields on every record (e.g. `role`); log calls cannot overwrite them. */
package/dist/log.js CHANGED
@@ -105,15 +105,16 @@ function expandSecret(value) {
105
105
  }
106
106
  return out;
107
107
  }
108
- /** Console format: `pretty` (pino-pretty, one line per record), `json`, or `auto` (pretty on a TTY). */
108
+ /** Console format: `pretty` (pino-pretty), `json`, or `auto` (pretty on a TTY). */
109
109
  export const LOG_FORMATS = ["auto", "pretty", "json"];
110
- /** Fields shown in the pretty prefix instead of the trailing object, in this order. */
111
- const PREFIX_FIELDS = ["reqId", "traceId", "jobId", "runId", "plugin"];
112
- /** pino-pretty's message format: `[role] [reqId=…] [plugin=…] msg`, each part only when present. */
110
+ /** Fields shown in the pretty prefix instead of the lines below, in this order. */
111
+ const PREFIX_FIELDS = ["traceId", "reqId", "queue", "jobId", "runId", "context", "plugin"];
112
+ /**
113
+ * pino-pretty's message format, the octok layout:
114
+ * `[pid=…] - [traceId=…] [reqId=…] [context=…] msg`, each bracket only when present.
115
+ */
113
116
  function prettyMessage(log, messageKey) {
114
- const parts = [];
115
- if (typeof log.role === "string")
116
- parts.push(`[${log.role}]`);
117
+ const parts = [`[pid=${process.pid}] -`];
117
118
  for (const k of PREFIX_FIELDS) {
118
119
  const v = log[k];
119
120
  if (typeof v === "string" || typeof v === "number")
@@ -122,6 +123,24 @@ function prettyMessage(log, messageKey) {
122
123
  parts.push(String(log[messageKey] ?? ""));
123
124
  return parts.join(" ");
124
125
  }
126
+ /** The HTTP access record (`http/app.ts`) as one sentence: method, path, status and duration. */
127
+ function foldAccessLine(log) {
128
+ if (log.msg !== "request" || typeof log.method !== "string" || typeof log.url !== "string") {
129
+ return log;
130
+ }
131
+ const { method, url, status, ms, ...rest } = log;
132
+ return { ...rest, msg: `${method} ${url} ${status} ${ms}ms` };
133
+ }
134
+ function colorsOn(opts, isTTY) {
135
+ if (opts.colors !== undefined)
136
+ return opts.colors;
137
+ const force = process.env.FORCE_COLOR;
138
+ if (force !== undefined && force !== "0")
139
+ return true;
140
+ if (process.env.NO_COLOR)
141
+ return false;
142
+ return isTTY;
143
+ }
125
144
  function consoleStream(opts) {
126
145
  if (opts.write) {
127
146
  const write = opts.write;
@@ -132,23 +151,50 @@ function consoleStream(opts) {
132
151
  if (!LOG_FORMATS.includes(format)) {
133
152
  throw new Error(`invalid log.format "${format}" (expected ${LOG_FORMATS.join(", ")})`);
134
153
  }
135
- const out = opts.out ?? process.stdout;
154
+ const target = opts.out ?? process.stdout;
155
+ // A console whose reader is gone (the `tee` of `pnpm dev:kernel` stopped by Ctrl-C, a closed
156
+ // pipe) fails every write with EPIPE. Unhandled, each failure is an uncaught exception the
157
+ // process guard logs, which writes again: an endless chain that starves the event loop. So the
158
+ // first stream error ends console output for good (Loki, when configured, still gets records).
159
+ let broken = false;
160
+ target.on?.("error", () => {
161
+ broken = true;
162
+ });
163
+ const out = {
164
+ write: (s) => {
165
+ if (!broken)
166
+ target.write(s);
167
+ },
168
+ };
136
169
  const isTTY = opts.isTTY ?? process.stdout.isTTY === true;
137
170
  const pretty = format === "pretty" || (format === "auto" && isTTY);
138
171
  if (!pretty)
139
172
  return { write: (s) => out.write(s) };
140
- // In process (no worker thread): pino-pretty formats each line synchronously.
173
+ const colors = colorsOn(opts, isTTY);
174
+ // In process (no worker thread): pino-pretty formats each record synchronously. The layout follows
175
+ // octok's buildPinoPrettyOptions: full local time, colored level, the bracket prefix, and
176
+ // the remaining fields as indented `key: value` lines below the message (errors with their stack).
141
177
  const format_ = prettyFactory({
142
- colorize: isTTY,
143
- colorizeObjects: isTTY,
144
- singleLine: true,
145
- // errors stay inside the one-line object instead of a multi-line stack block
146
- errorLikeObjectKeys: [],
147
- translateTime: "SYS:HH:MM:ss.l",
178
+ colorize: colors,
179
+ colorizeObjects: colors,
180
+ singleLine: false,
181
+ translateTime: "SYS:standard",
148
182
  ignore: ["pid", "hostname", "role", "spanId", ...PREFIX_FIELDS].join(","),
149
183
  messageFormat: (log, messageKey) => prettyMessage(log, messageKey),
150
184
  });
151
- return { write: (s) => out.write(format_(s)) };
185
+ return {
186
+ write: (s) => {
187
+ let record;
188
+ try {
189
+ record = JSON.parse(s);
190
+ }
191
+ catch {
192
+ out.write(s);
193
+ return;
194
+ }
195
+ out.write(format_(foldAccessLine(record)));
196
+ },
197
+ };
152
198
  }
153
199
  /**
154
200
  * Creates the kernel logger on pino. Each record is `{ time, level, msg, ...base, ...fields }` with
@@ -254,7 +300,7 @@ export function createLogger(level = "info", opts = {}) {
254
300
  }
255
301
  sink[lvl](obj, redactMsg(msg));
256
302
  };
257
- return {
303
+ const root = {
258
304
  debug: (msg, fields) => emit("debug", msg, fields),
259
305
  info: (msg, fields) => emit("info", msg, fields),
260
306
  warn: (msg, fields) => emit("warn", msg, fields),
@@ -299,5 +345,35 @@ export function createLogger(level = "info", opts = {}) {
299
345
  }
300
346
  }
301
347
  },
348
+ // a method, so a wrapper of this logger (a test proxy) is the parent its children forward to
349
+ child(fields) {
350
+ return childLogger(this, fields);
351
+ },
352
+ };
353
+ return root;
354
+ }
355
+ /**
356
+ * A logger that adds `bound` to every record and forwards it to `parent`'s public methods (so a spy
357
+ * on the parent sees its children's records); call fields cannot overwrite the bound ones.
358
+ */
359
+ function childLogger(parent, bound) {
360
+ const merged = (fields) => ({ ...fields, ...bound });
361
+ const child = {
362
+ debug: (msg, fields) => parent.debug(msg, merged(fields)),
363
+ info: (msg, fields) => parent.info(msg, merged(fields)),
364
+ warn: (msg, fields) => parent.warn(msg, merged(fields)),
365
+ error: (msg, fields) => parent.error(msg, merged(fields)),
366
+ get level() {
367
+ return parent.level;
368
+ },
369
+ setLevel: (level) => parent.setLevel(level),
370
+ addSecrets: (values) => parent.addSecrets(values),
371
+ addOrgSecrets: (values) => parent.addOrgSecrets(values),
372
+ redact: (text) => parent.redact(text),
373
+ flush: () => parent.flush(),
374
+ child(fields) {
375
+ return childLogger(this, fields);
376
+ },
302
377
  };
378
+ return child;
303
379
  }
@@ -35,6 +35,13 @@ export declare const KERNEL_PURPOSES: readonly [{
35
35
  readonly zh: "会话标题";
36
36
  readonly en: "Conversation titles";
37
37
  };
38
+ }, {
39
+ readonly purpose: "web_fetch";
40
+ readonly kind: "chat";
41
+ readonly label: {
42
+ readonly zh: "网页提取";
43
+ readonly en: "Web page extraction";
44
+ };
38
45
  }, {
39
46
  readonly purpose: "subagent_fast";
40
47
  readonly kind: "chat";
@@ -140,6 +147,19 @@ export declare class ModelPurposesService {
140
147
  setOrg(orgId: string, purpose: string, target: PurposeTarget | null, actor: string, actorTokenId?: string): Promise<PurposeView>;
141
148
  /** 生效的映射:组织覆盖,否则部署默认;都没有 → `model.unconfigured`;用途未声明 → `model.purpose_unknown`。 */
142
149
  resolve(orgId: string, purpose: string): Promise<PurposeTarget>;
150
+ /** The target a {@link chat} call on `purpose` uses: the mapping, else `fallback` when unmapped. */
151
+ private chatTarget;
152
+ /**
153
+ * The limits of the model a {@link chat} call on `purpose` would use now (the same mapping and
154
+ * `fallback` rule): its context window and output cap, so a caller can size its request (e.g.
155
+ * the `WebFetch` page) before the call. Fails as `chat` does before sending.
156
+ */
157
+ chatLimits(orgId: string, purpose: string, opts?: {
158
+ fallback?: PurposeTarget;
159
+ }): Promise<{
160
+ contextWindow: number;
161
+ maxOutputTokens: number;
162
+ }>;
143
163
  /**
144
164
  * 按用途发一次对话请求:解析映射、按目标模型的声明宽松处理参数(补默认值,与运行时一致),用 `consumeModelStream`
145
165
  * 收完(只有流空闲看门狗)。输出上限取 `min(req.maxOutputTokens, 目标模型的 maxOutputTokens)`。
@@ -147,10 +167,13 @@ export declare class ModelPurposesService {
147
167
  * `opts.fallback`: the target used when the purpose is not mapped (`model.unconfigured`).
148
168
  * `opts.beforeSend`: called with the resolved target and the final request just before sending;
149
169
  * what it throws is thrown as is (a pre-send failure, e.g. a denied `model.request` hook).
170
+ * `opts.paramDefaults`: values for parameters the target does not set, kept only where the
171
+ * target model declares the key and the value fits it (e.g. `thinking: "off"` for a short call).
150
172
  */
151
173
  chat(orgId: string, purpose: string, req: Omit<ModelRequest, "model" | "params">, signal: AbortSignal, opts?: {
152
174
  fallback?: PurposeTarget;
153
175
  beforeSend?: (target: ModelRef, request: ModelRequest) => Promise<void>;
176
+ paramDefaults?: Record<string, ParamValue>;
154
177
  }): Promise<{
155
178
  content: ContentBlock[];
156
179
  stopReason: StopReason;