mixdog 1.3.4 → 1.3.5

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 (197) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +2 -1
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/bot-hops.mjs +62 -2
  128. package/src/runtime/shared/config.mjs +49 -22
  129. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  130. package/src/runtime/shared/connector-catalog.mjs +16 -8
  131. package/src/runtime/shared/developer-options.mjs +32 -1
  132. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  133. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  134. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  135. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  136. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  137. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  138. package/src/runtime/shared/provider-accounts.mjs +224 -58
  139. package/src/runtime/shared/provider-api-key.mjs +19 -8
  140. package/src/runtime/shared/routine-trigger.mjs +42 -0
  141. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  142. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  143. package/src/runtime/shared/user-data-guard.mjs +30 -2
  144. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  145. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  146. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  147. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  148. package/src/session-runtime/boot/begin.mjs +3 -2
  149. package/src/session-runtime/boot/tools.mjs +2 -0
  150. package/src/session-runtime/bot-chain.mjs +32 -0
  151. package/src/session-runtime/bot-status.mjs +7 -4
  152. package/src/session-runtime/channel-config-api.mjs +63 -17
  153. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  154. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  155. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  156. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  157. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  158. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  159. package/src/session-runtime/owned-automation-run.mjs +33 -12
  160. package/src/session-runtime/provider-auth/login.mjs +7 -0
  161. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  162. package/src/session-runtime/routine-home-turn.mjs +33 -6
  163. package/src/session-runtime/schedule-session-run.mjs +7 -3
  164. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  165. package/src/session-runtime/services/channel-admin.mjs +66 -18
  166. package/src/session-runtime/services/channel-worker.mjs +1 -1
  167. package/src/session-runtime/services/provider-admin.mjs +128 -25
  168. package/src/session-runtime/services/schedule-access.mjs +14 -0
  169. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  170. package/src/session-runtime/session-create/session-options.mjs +7 -4
  171. package/src/session-runtime/settings-system-api.mjs +16 -1
  172. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  173. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  174. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  175. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  176. package/src/session-runtime/task-session-route.mjs +99 -0
  177. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  178. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  179. package/src/session-runtime/tool-surface.mjs +4 -0
  180. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  181. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  182. package/src/session-runtime/workspace/transitions.mjs +10 -2
  183. package/src/standalone/daemon-session-control.mjs +88 -20
  184. package/src/standalone/daemon.mjs +8 -0
  185. package/src/standalone/session-protocol.mjs +5 -0
  186. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  187. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  188. package/src/standalone/session-service.mjs +13 -0
  189. package/src/standalone/session-tool-output.mjs +8 -5
  190. package/src/tui/app/model-options.mjs +2 -0
  191. package/src/tui/dist/index.mjs +24 -2
  192. package/src/tui/session/notice-surface.mjs +17 -1
  193. package/src/tui/session/session-api/integrations.mjs +1 -0
  194. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  195. package/src/tui/session/session-api/settings.mjs +14 -2
  196. package/src/tui/session/turn-stream.mjs +17 -1
  197. package/src/tui/session/turn.mjs +22 -2
@@ -0,0 +1,560 @@
1
+ // Devin Connect transport over fetch: unary RPCs (`application/proto`) and
2
+ // server-streaming RPCs (`application/connect+proto` envelopes).
3
+ //
4
+ // Every failure surfaces as DevinWireError { code, status, retryable, message }.
5
+ import { gunzipSync, gzipSync } from 'node:zlib';
6
+ import { decodeDevinMessage, encodeDevinMessage } from './devin-wire-protobuf.mjs';
7
+ import { retryAfterMsFromError } from './retry-classifier.mjs';
8
+
9
+ export const DEVIN_COMPRESSED_FLAG = 0x01;
10
+ export const DEVIN_END_STREAM_FLAG = 0x02;
11
+ // Advertised envelope length cap: the 4-byte prefix is peer-controlled, so an
12
+ // oversize length fails on the header instead of buffering the payload.
13
+ export const DEVIN_MAX_FRAME_BYTES = 16 * 1024 * 1024;
14
+ // Separate cap on what one gzip payload may inflate to.
15
+ export const DEVIN_MAX_DECOMPRESSED_BYTES = 32 * 1024 * 1024;
16
+ const FRAME_HEADER_BYTES = 5;
17
+ const MAX_ERROR_DETAIL_CHARS = 2000;
18
+
19
+ export const DEVIN_STREAM_HEADERS = Object.freeze({
20
+ 'content-type': 'application/connect+proto',
21
+ 'connect-protocol-version': '1',
22
+ 'connect-content-encoding': 'gzip',
23
+ 'connect-accept-encoding': 'gzip',
24
+ 'accept-encoding': 'identity',
25
+ 'user-agent': 'connect-go/1.18.1 (go1.26.3)',
26
+ });
27
+
28
+ export const DEVIN_UNARY_HEADERS = Object.freeze({
29
+ 'content-type': 'application/proto',
30
+ 'connect-protocol-version': '1',
31
+ accept: '*/*',
32
+ });
33
+
34
+ export class DevinWireError extends Error {
35
+ constructor(message, { code, status = 0, retryable = false, connectCode = '', detail, retryAfterMs, cause } = {}) {
36
+ super(message, cause === undefined ? undefined : { cause });
37
+ this.name = 'DevinWireError';
38
+ this.code = code;
39
+ this.status = status;
40
+ this.retryable = retryable;
41
+ if (connectCode) this.connectCode = connectCode;
42
+ if (detail) this.detail = detail;
43
+ if (Number.isFinite(retryAfterMs)) this.retryAfterMs = retryAfterMs;
44
+ }
45
+ }
46
+
47
+ // Connect code -> [Mixdog code, HTTP-equivalent status, retryable].
48
+ const CONNECT_CODES = Object.freeze({
49
+ canceled: ['aborted', 499, false],
50
+ unknown: ['internal', 500, true],
51
+ invalid_argument: ['invalid_argument', 400, false],
52
+ deadline_exceeded: ['unavailable', 504, true],
53
+ not_found: ['not_found', 404, false],
54
+ already_exists: ['invalid_argument', 409, false],
55
+ permission_denied: ['permission', 403, false],
56
+ resource_exhausted: ['resource_exhausted', 429, true],
57
+ failed_precondition: ['invalid_argument', 400, false],
58
+ aborted: ['unavailable', 409, true],
59
+ out_of_range: ['invalid_argument', 400, false],
60
+ unimplemented: ['not_found', 501, false],
61
+ internal: ['internal', 500, true],
62
+ unavailable: ['unavailable', 503, true],
63
+ data_loss: ['internal', 500, true],
64
+ unauthenticated: ['unauthenticated', 401, false],
65
+ });
66
+
67
+ const HTTP_STATUS_CONNECT_CODE = Object.freeze({
68
+ 400: 'invalid_argument',
69
+ 401: 'unauthenticated',
70
+ 403: 'permission_denied',
71
+ 404: 'not_found',
72
+ 408: 'deadline_exceeded',
73
+ 429: 'resource_exhausted',
74
+ 502: 'unavailable',
75
+ 503: 'unavailable',
76
+ 504: 'deadline_exceeded',
77
+ });
78
+
79
+ // Size wording turns resource_exhausted into request_too_large; throttle and
80
+ // quota wording always wins ("request exceeded the rate limit" is a throttle).
81
+ const TOO_LARGE_RE =
82
+ /\b(?:too\s+(?:large|big|long)|exceeds?\s+(?:the\s+)?(?:maximum|max|size|length)|(?:payload|message|request|prompt|context)\s+(?:size|length)\b|maximum\s+(?:request|message|payload)\s+size)/i;
83
+ const THROTTLE_RE = /too\s+many\s+requests|quota|rate[\s_-]*limit|throttl|credits?|capacity/i;
84
+ // Devin words two throttles under other codes, as the reference executor
85
+ // (CLIProxyAPI devin_wire.go) maps them to 429: a model "in high demand"
86
+ // arrives as permission_denied, a spent quota as failed_precondition.
87
+ const HIGH_DEMAND_RE = /high demand/i;
88
+ const SPENT_QUOTA_RE = /quota|credit|acu|exhausted|limit/i;
89
+
90
+ // The throttle's [Mixdog code, HTTP-equivalent status, retryable] for a
91
+ // throttle Devin sent under a non-throttle code; null otherwise.
92
+ function rewordedThrottle(code, detail) {
93
+ if (code === 'permission_denied' && HIGH_DEMAND_RE.test(detail)) return CONNECT_CODES.resource_exhausted;
94
+ if (code === 'failed_precondition' && SPENT_QUOTA_RE.test(detail)) return CONNECT_CODES.resource_exhausted;
95
+ return null;
96
+ }
97
+
98
+ function sanitizeDetail(text) {
99
+ const normalized = String(text || '')
100
+ .replace(/\s+/g, ' ')
101
+ .trim();
102
+ if (!normalized || /^(?:<!doctype\s+html\b|<html\b)/i.test(normalized)) return '';
103
+ // Control characters or broken surrogates mean binary or hostile output.
104
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: detects control characters in untrusted server text
105
+ if (/[\u0000-\u001f\u007f-\u009f]/.test(normalized) || !normalized.isWellFormed()) return '';
106
+ if (normalized.length <= MAX_ERROR_DETAIL_CHARS) return normalized;
107
+ const cut = /[\ud800-\udbff]/.test(normalized[MAX_ERROR_DETAIL_CHARS - 1])
108
+ ? MAX_ERROR_DETAIL_CHARS - 1
109
+ : MAX_ERROR_DETAIL_CHARS;
110
+ return `${normalized.slice(0, cut)}…`;
111
+ }
112
+
113
+ const REDACTED = '[redacted]';
114
+ // Credential shapes redacted even when the request did not carry them.
115
+ const CREDENTIAL_RE = /devin-session-token\$[^\s"'<>,;]+|\bBearer\s+[^\s"'<>,;]+|\beyJ[\w-]+\.[\w-]+\.[\w-]*/gi;
116
+
117
+ // Server diagnostics may echo what the request carried; every known secret
118
+ // and every credential-shaped token is replaced before text is surfaced.
119
+ function redactCredentials(text, secrets) {
120
+ let out = String(text ?? '');
121
+ for (const secret of secrets) out = out.replaceAll(secret, REDACTED);
122
+ return out.replace(CREDENTIAL_RE, REDACTED);
123
+ }
124
+
125
+ // Every credential a request carries (`Metadata.apiKey`, `Metadata.userJwt`,
126
+ // assignment JWTs, credential headers), plus the bare token behind a scheme
127
+ // prefix (`devin-session-token$…`, `Bearer …`). Longest first so a secret that
128
+ // contains another is replaced whole.
129
+ function requestSecrets(message, headers) {
130
+ const secrets = new Set();
131
+ const add = (value) => {
132
+ const text = typeof value === 'string' ? value.trim() : '';
133
+ for (const candidate of [text, text.slice(text.lastIndexOf('$') + 1), text.replace(/^Bearer\s+/i, '')]) {
134
+ if (candidate) secrets.add(candidate);
135
+ }
136
+ };
137
+ for (const source of [message, message?.metadata]) {
138
+ for (const [key, value] of Object.entries(source || {})) {
139
+ if (/(?:apiKey|Jwt|Token)$/i.test(key)) add(value);
140
+ }
141
+ }
142
+ for (const [name, value] of Object.entries(headers || {})) {
143
+ if (/authorization|cookie|token|jwt|api[-_]?key/i.test(name)) add(value);
144
+ }
145
+ return [...secrets].sort((a, b) => b.length - a.length);
146
+ }
147
+
148
+ function summarizeDetails(details, secrets) {
149
+ if (!Array.isArray(details)) return '';
150
+ const parts = [];
151
+ for (const entry of details) {
152
+ if (!entry || typeof entry !== 'object') continue;
153
+ const type = typeof entry.type === 'string' ? entry.type : '';
154
+ let evidence = typeof entry.value === 'string' ? entry.value : '';
155
+ if (entry.debug !== undefined) {
156
+ try {
157
+ evidence = typeof entry.debug === 'string' ? entry.debug : JSON.stringify(entry.debug);
158
+ } catch {}
159
+ }
160
+ const part = [type, evidence].filter(Boolean).join(': ');
161
+ if (part) parts.push(part);
162
+ }
163
+ return sanitizeDetail(redactCredentials(parts.join('; '), secrets));
164
+ }
165
+
166
+ function retryAfterMsOf(headers) {
167
+ const value = headers?.get?.('retry-after');
168
+ if (!value) return undefined;
169
+ const ms = retryAfterMsFromError({ headers: { 'retry-after': value } });
170
+ return Number.isFinite(ms) ? ms : undefined;
171
+ }
172
+
173
+ function connectError(
174
+ { connectCode = '', message = '', details, status = 0, headers, operation = 'request' },
175
+ secrets
176
+ ) {
177
+ let code = String(connectCode || HTTP_STATUS_CONNECT_CODE[status] || '').toLowerCase();
178
+ // Server-chosen codes outside the Connect vocabulary are never surfaced.
179
+ if (code && !Object.hasOwn(CONNECT_CODES, code)) code = 'unknown';
180
+ if (!code && status >= 500) code = 'internal';
181
+ if (!code && status === 413) code = 'resource_exhausted';
182
+ const detail = sanitizeDetail(redactCredentials(message, secrets));
183
+ const reworded = rewordedThrottle(code, detail);
184
+ const [mixdogCode, mappedStatus, retryable] = reworded ||
185
+ CONNECT_CODES[code] || ['internal', status || 500, status >= 500];
186
+ const detailsSummary = summarizeDetails(details, secrets);
187
+ const tooLarge =
188
+ status === 413 ||
189
+ (mixdogCode === 'resource_exhausted' && detail && !THROTTLE_RE.test(detail) && TOO_LARGE_RE.test(detail));
190
+ const finalCode = tooLarge ? 'request_too_large' : mixdogCode;
191
+ const head = `Devin ${operation} ${code || 'error'}${status ? ` (HTTP ${status})` : ''}`;
192
+ const text = [head, detail].filter(Boolean).join(': ') + (detailsSummary ? ` [details: ${detailsSummary}]` : '');
193
+ const error = new DevinWireError(text, {
194
+ code: finalCode,
195
+ // A reworded throttle reports its throttle status, not the carrier's.
196
+ status: tooLarge ? 413 : (reworded ? mappedStatus : status || mappedStatus),
197
+ retryable: tooLarge ? false : retryable,
198
+ connectCode: code,
199
+ detail: [detail, detailsSummary].filter(Boolean).join(' '),
200
+ retryAfterMs: retryAfterMsOf(headers),
201
+ });
202
+ return error;
203
+ }
204
+
205
+ // Builds the error for a Connect `{ code, message, details }` object, an HTTP
206
+ // status, or both. `status` is the real HTTP status when there was one.
207
+ // Credential-shaped tokens in server text are redacted.
208
+ export function devinConnectError(fields) {
209
+ return connectError(fields, []);
210
+ }
211
+
212
+ function protocolError(message, { retryable = false, cause } = {}) {
213
+ return new DevinWireError(`Devin protocol error: ${message}`, { code: 'protocol', retryable, cause });
214
+ }
215
+
216
+ // Requests carry credentials in the body, so a redirect is never followed:
217
+ // the body would be replayed to a host nobody validated.
218
+ function redirectError(status = 0) {
219
+ return new DevinWireError('Devin server redirected the request; redirects are refused', {
220
+ code: 'protocol',
221
+ status,
222
+ retryable: false,
223
+ });
224
+ }
225
+
226
+ function isAbort(error, signal) {
227
+ return signal?.aborted || error?.name === 'AbortError' || error?.code === 'ABORT_ERR';
228
+ }
229
+
230
+ function abortedError(cause) {
231
+ return new DevinWireError('Devin request aborted', { code: 'aborted', status: 499, retryable: false, cause });
232
+ }
233
+
234
+ // A copy of a transport error chain with credentials redacted from every message.
235
+ function redactedCause(error, secrets, depth = 0) {
236
+ if (!(error instanceof Error) || depth > 4) return undefined;
237
+ const copy = new Error(redactCredentials(error.message, secrets));
238
+ copy.name = error.name;
239
+ if (typeof error.code === 'string') copy.code = error.code;
240
+ const inner = redactedCause(error.cause, secrets, depth + 1);
241
+ if (inner) copy.cause = inner;
242
+ return copy;
243
+ }
244
+
245
+ // Normalizes anything thrown by fetch/body reading into a DevinWireError.
246
+ function transportError(error, signal, secrets) {
247
+ if (error instanceof DevinWireError) return error;
248
+ if (isAbort(error, signal)) return abortedError(error);
249
+ // fetch with `redirect: 'error'` rejects a redirect as a network error.
250
+ if (error?.cause?.message === 'unexpected redirect') return redirectError();
251
+ return new DevinWireError(`Devin connection failed: ${redactCredentials(error?.message || error, secrets)}`, {
252
+ code: 'unavailable',
253
+ status: 0,
254
+ retryable: true,
255
+ cause: redactedCause(error, secrets),
256
+ });
257
+ }
258
+
259
+ function inflateSync(payload, maxDecompressedBytes) {
260
+ try {
261
+ return new Uint8Array(gunzipSync(payload, { maxOutputLength: maxDecompressedBytes }));
262
+ } catch (error) {
263
+ if (error?.code === 'ERR_BUFFER_TOO_LARGE') {
264
+ throw protocolError(`decompressed payload exceeds ${maxDecompressedBytes}-byte cap`, { cause: error });
265
+ }
266
+ throw protocolError('invalid gzip payload', { cause: error });
267
+ }
268
+ }
269
+
270
+ function decodeOrThrow(typeName, bytes) {
271
+ try {
272
+ return decodeDevinMessage(typeName, bytes);
273
+ } catch (error) {
274
+ throw protocolError(`malformed ${typeName}: ${error.message}`, { cause: error });
275
+ }
276
+ }
277
+
278
+ export function encodeDevinFrame(payload, flags = 0) {
279
+ const frame = new Uint8Array(FRAME_HEADER_BYTES + payload.length);
280
+ frame[0] = flags;
281
+ new DataView(frame.buffer).setUint32(1, payload.length, false);
282
+ frame.set(payload, FRAME_HEADER_BYTES);
283
+ return frame;
284
+ }
285
+
286
+ // Incremental envelope reader. Chunks are kept as received and joined only
287
+ // once a frame's announced length has fully arrived.
288
+ export function createDevinFrameReader({ maxFrameBytes = DEVIN_MAX_FRAME_BYTES } = {}) {
289
+ const chunks = [];
290
+ let buffered = 0;
291
+
292
+ const take = (size) => {
293
+ const out = new Uint8Array(size);
294
+ let offset = 0;
295
+ while (offset < size) {
296
+ const chunk = chunks[0];
297
+ const needed = size - offset;
298
+ if (chunk.length <= needed) {
299
+ out.set(chunk, offset);
300
+ offset += chunk.length;
301
+ chunks.shift();
302
+ } else {
303
+ out.set(chunk.subarray(0, needed), offset);
304
+ chunks[0] = chunk.subarray(needed);
305
+ offset = size;
306
+ }
307
+ }
308
+ buffered -= size;
309
+ return out;
310
+ };
311
+
312
+ const peekHeader = () => {
313
+ const header = new Uint8Array(FRAME_HEADER_BYTES);
314
+ let offset = 0;
315
+ for (const chunk of chunks) {
316
+ const part = chunk.subarray(0, FRAME_HEADER_BYTES - offset);
317
+ header.set(part, offset);
318
+ offset += part.length;
319
+ if (offset === FRAME_HEADER_BYTES) break;
320
+ }
321
+ return header;
322
+ };
323
+
324
+ return {
325
+ // Returns every frame completed by `chunk`, in wire order.
326
+ push(chunk) {
327
+ if (chunk?.length) {
328
+ chunks.push(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk));
329
+ buffered += chunk.length;
330
+ }
331
+ const frames = [];
332
+ while (buffered >= FRAME_HEADER_BYTES) {
333
+ const header = peekHeader();
334
+ const flags = header[0];
335
+ const length = new DataView(header.buffer).getUint32(1, false);
336
+ if (length > maxFrameBytes) {
337
+ throw protocolError(`frame length ${length} exceeds ${maxFrameBytes}-byte cap`);
338
+ }
339
+ if (buffered < FRAME_HEADER_BYTES + length) break;
340
+ take(FRAME_HEADER_BYTES);
341
+ frames.push({ flags, payload: take(length) });
342
+ }
343
+ return frames;
344
+ },
345
+ get bufferedBytes() {
346
+ return buffered;
347
+ },
348
+ };
349
+ }
350
+
351
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
352
+ const lenientTextDecoder = new TextDecoder('utf-8');
353
+
354
+ // A trailer that is not JSON carries no error: the stream simply ended.
355
+ // Tolerated on purpose: established clients of this service accept such a
356
+ // trailer, and whether the server ever sends one cannot be ruled out, so
357
+ // rejecting it could fail every turn. Completion still requires the
358
+ // end-of-stream frame itself.
359
+ function readTrailer(bytes, operation, secrets) {
360
+ let parsed;
361
+ try {
362
+ parsed = JSON.parse(lenientTextDecoder.decode(bytes).trim());
363
+ } catch {
364
+ return null;
365
+ }
366
+ const error = parsed?.error;
367
+ if (!error || typeof error !== 'object') return null;
368
+ const connectCode = typeof error.code === 'string' ? error.code : '';
369
+ const message = typeof error.message === 'string' ? error.message : '';
370
+ if (!connectCode && !message) return null;
371
+ const metadata = parsed.metadata && typeof parsed.metadata === 'object' ? parsed.metadata : {};
372
+ const retryAfter = [metadata['retry-after']].flat()[0];
373
+ return connectError(
374
+ {
375
+ connectCode: connectCode || 'unknown',
376
+ message,
377
+ details: error.details,
378
+ operation,
379
+ headers: retryAfter == null ? undefined : new Headers({ 'retry-after': String(retryAfter) }),
380
+ },
381
+ secrets
382
+ );
383
+ }
384
+
385
+ // Parses an end-of-stream payload; returns the DevinWireError it carries or null.
386
+ export function parseDevinTrailer(bytes, operation = 'stream') {
387
+ return readTrailer(bytes, operation, []);
388
+ }
389
+
390
+ function requestUrl(baseUrl, path) {
391
+ return `${String(baseUrl).replace(/\/+$/, '')}${path}`;
392
+ }
393
+
394
+ async function readBody(response, maxBytes, signal, secrets) {
395
+ if (!response.body) return new Uint8Array(0);
396
+ const reader = response.body.getReader();
397
+ const parts = [];
398
+ let total = 0;
399
+ try {
400
+ for (;;) {
401
+ const { done, value } = await reader.read();
402
+ if (done) break;
403
+ total += value.length;
404
+ if (total > maxBytes) throw protocolError(`response body exceeds ${maxBytes}-byte cap`);
405
+ parts.push(value);
406
+ }
407
+ } catch (error) {
408
+ await reader.cancel().catch(() => {});
409
+ throw transportError(error, signal, secrets);
410
+ }
411
+ const out = new Uint8Array(total);
412
+ let offset = 0;
413
+ for (const part of parts) {
414
+ out.set(part, offset);
415
+ offset += part.length;
416
+ }
417
+ return out;
418
+ }
419
+
420
+ // Error body of a failed HTTP response: Connect JSON `{ code, message }` when present.
421
+ async function httpError(response, operation, signal, secrets) {
422
+ let connectCode = '';
423
+ let message = '';
424
+ let details;
425
+ try {
426
+ const bytes = await readBody(response, 64 * 1024, signal, secrets);
427
+ const contentType = response.headers.get('content-type') || '';
428
+ if (!/text\/html/i.test(contentType)) {
429
+ const text = textDecoder.decode(bytes).trim();
430
+ try {
431
+ const parsed = JSON.parse(text);
432
+ if (typeof parsed?.code === 'string') connectCode = parsed.code;
433
+ message = [parsed?.message, parsed?.error, parsed?.error?.message].find((v) => typeof v === 'string') ?? '';
434
+ details = parsed?.details;
435
+ } catch {
436
+ message = text;
437
+ }
438
+ }
439
+ } catch (error) {
440
+ if (error instanceof DevinWireError && error.code === 'aborted') throw error;
441
+ }
442
+ return connectError(
443
+ { connectCode, message, details, status: response.status, headers: response.headers, operation },
444
+ secrets
445
+ );
446
+ }
447
+
448
+ async function post({ baseUrl, path, body, headers, fetchFn, signal, secrets }) {
449
+ if (signal?.aborted) throw abortedError(signal.reason);
450
+ let response;
451
+ try {
452
+ response = await fetchFn(requestUrl(baseUrl, path), { method: 'POST', headers, body, signal, redirect: 'error' });
453
+ } catch (error) {
454
+ throw transportError(error, signal, secrets);
455
+ }
456
+ // A fetch that hands back a redirect instead of rejecting it is refused too.
457
+ if (response.status >= 300 && response.status < 400) {
458
+ await response.body?.cancel().catch(() => {});
459
+ throw redirectError(response.status);
460
+ }
461
+ return response;
462
+ }
463
+
464
+ function operationName(path) {
465
+ return String(path).split('/').filter(Boolean).pop() || 'request';
466
+ }
467
+
468
+ export async function devinUnary({
469
+ baseUrl,
470
+ path,
471
+ requestType,
472
+ responseType,
473
+ message = {},
474
+ headers = {},
475
+ fetchFn = fetch,
476
+ signal,
477
+ maxResponseBytes = DEVIN_MAX_FRAME_BYTES,
478
+ maxDecompressedBytes = DEVIN_MAX_DECOMPRESSED_BYTES,
479
+ }) {
480
+ const operation = operationName(path);
481
+ const secrets = requestSecrets(message, headers);
482
+ const response = await post({
483
+ baseUrl,
484
+ path,
485
+ body: encodeDevinMessage(requestType, message),
486
+ headers: { ...DEVIN_UNARY_HEADERS, ...headers },
487
+ fetchFn,
488
+ signal,
489
+ secrets,
490
+ });
491
+ if (!response.ok) throw await httpError(response, operation, signal, secrets);
492
+ let bytes = await readBody(response, maxResponseBytes, signal, secrets);
493
+ // Some edges return a gzip body without Content-Encoding. A protobuf message
494
+ // can never start with 0x1f (wire type 7), so the magic is unambiguous.
495
+ if (bytes[0] === 0x1f && bytes[1] === 0x8b) bytes = inflateSync(bytes, maxDecompressedBytes);
496
+ return decodeOrThrow(responseType, bytes);
497
+ }
498
+
499
+ export async function* devinStream({
500
+ baseUrl,
501
+ path,
502
+ requestType,
503
+ responseType,
504
+ message = {},
505
+ headers = {},
506
+ fetchFn = fetch,
507
+ signal,
508
+ maxFrameBytes = DEVIN_MAX_FRAME_BYTES,
509
+ maxDecompressedBytes = DEVIN_MAX_DECOMPRESSED_BYTES,
510
+ }) {
511
+ const operation = operationName(path);
512
+ const secrets = requestSecrets(message, headers);
513
+ const body = encodeDevinFrame(gzipSync(encodeDevinMessage(requestType, message)), DEVIN_COMPRESSED_FLAG);
514
+ const response = await post({
515
+ baseUrl,
516
+ path,
517
+ body,
518
+ headers: { ...DEVIN_STREAM_HEADERS, ...headers },
519
+ fetchFn,
520
+ signal,
521
+ secrets,
522
+ });
523
+ if (!response.ok) throw await httpError(response, operation, signal, secrets);
524
+ if (!response.body) throw protocolError('response body is empty', { retryable: true });
525
+
526
+ const reader = response.body.getReader();
527
+ const frames = createDevinFrameReader({ maxFrameBytes });
528
+ // Whether the end-of-stream trailer arrived. The body is read to its end: an
529
+ // error-free trailer does not stop it, but a body that stops without one (a
530
+ // frame boundary included) was cut short.
531
+ let terminated = false;
532
+ try {
533
+ for (;;) {
534
+ let chunk;
535
+ try {
536
+ chunk = await reader.read();
537
+ } catch (error) {
538
+ throw transportError(error, signal, secrets);
539
+ }
540
+ if (chunk.done) break;
541
+ // Only the compressed and end-of-stream bits carry meaning; others are ignored.
542
+ for (const { flags, payload } of frames.push(chunk.value)) {
543
+ const bytes = flags & DEVIN_COMPRESSED_FLAG ? inflateSync(payload, maxDecompressedBytes) : payload;
544
+ if (flags & DEVIN_END_STREAM_FLAG) {
545
+ const trailerError = readTrailer(bytes, operation, secrets);
546
+ if (trailerError) throw trailerError;
547
+ terminated = true;
548
+ continue;
549
+ }
550
+ yield decodeOrThrow(responseType, bytes);
551
+ }
552
+ }
553
+ if (frames.bufferedBytes) {
554
+ throw protocolError(`stream ended inside a frame (${frames.bufferedBytes} bytes buffered)`, { retryable: true });
555
+ }
556
+ if (!terminated) throw protocolError('stream ended without an end-of-stream trailer', { retryable: true });
557
+ } finally {
558
+ await reader.cancel().catch(() => {});
559
+ }
560
+ }