@adhdev/daemon-core 0.9.82-rc.14 → 0.9.82-rc.140

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 (115) hide show
  1. package/dist/chat/source-machine.d.ts +166 -0
  2. package/dist/chat/source-resolver.d.ts +104 -0
  3. package/dist/chat/subscription-updates.d.ts +1 -0
  4. package/dist/cli-adapter-types.d.ts +5 -1
  5. package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
  6. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  7. package/dist/cli-adapters/provider-cli-adapter.d.ts +87 -63
  8. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  9. package/dist/cli-adapters/provider-cli-shared.d.ts +21 -0
  10. package/dist/commands/router.d.ts +22 -0
  11. package/dist/config/chat-history.d.ts +5 -0
  12. package/dist/config/config.d.ts +5 -0
  13. package/dist/config/mesh-config.d.ts +68 -1
  14. package/dist/git/git-commands.d.ts +5 -1
  15. package/dist/index.d.ts +18 -6
  16. package/dist/index.js +10431 -2827
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +10376 -2811
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/installer.d.ts +1 -4
  21. package/dist/launch.d.ts +1 -1
  22. package/dist/logging/async-batch-writer.d.ts +10 -0
  23. package/dist/mesh/beads-db.d.ts +72 -0
  24. package/dist/mesh/contracts.d.ts +164 -0
  25. package/dist/mesh/coordinator-registry.d.ts +25 -0
  26. package/dist/mesh/mesh-active-work.d.ts +90 -0
  27. package/dist/mesh/mesh-events.d.ts +77 -5
  28. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  29. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  30. package/dist/mesh/mesh-ledger.d.ts +58 -1
  31. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  32. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  33. package/dist/mesh/preview-freshness.d.ts +18 -0
  34. package/dist/mesh/refine-config.d.ts +193 -0
  35. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  36. package/dist/providers/approval-utils.d.ts +9 -0
  37. package/dist/providers/chat-message-normalization.d.ts +1 -0
  38. package/dist/providers/cli-provider-instance.d.ts +6 -1
  39. package/dist/providers/contracts.d.ts +19 -0
  40. package/dist/providers/read-chat-contract.d.ts +29 -0
  41. package/dist/providers/transcript-v2.d.ts +176 -0
  42. package/dist/repo-mesh-types.d.ts +67 -0
  43. package/dist/shared-types.d.ts +12 -0
  44. package/dist/status/reporter.d.ts +2 -0
  45. package/dist/status/snapshot.d.ts +1 -0
  46. package/dist/types.d.ts +5 -0
  47. package/package.json +3 -1
  48. package/src/boot/daemon-lifecycle.ts +3 -0
  49. package/src/chat/source-machine.ts +534 -0
  50. package/src/chat/source-resolver.ts +0 -0
  51. package/src/chat/subscription-updates.ts +14 -1
  52. package/src/cli-adapter-types.d.ts +1 -0
  53. package/src/cli-adapter-types.ts +3 -1
  54. package/src/cli-adapters/cli-script-runner.ts +145 -0
  55. package/src/cli-adapters/cli-state-engine.ts +1083 -0
  56. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  57. package/src/cli-adapters/provider-cli-adapter.ts +630 -1137
  58. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  59. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  60. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  61. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  62. package/src/cli-adapters/provider-cli-shared.ts +51 -11
  63. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  64. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  65. package/src/commands/chat-commands.ts +1428 -50
  66. package/src/commands/cli-manager.ts +145 -3
  67. package/src/commands/handler.ts +8 -1
  68. package/src/commands/mesh-coordinator.ts +13 -143
  69. package/src/commands/router.ts +3271 -427
  70. package/src/config/chat-history.ts +79 -24
  71. package/src/config/config.ts +12 -0
  72. package/src/config/mesh-config.ts +249 -2
  73. package/src/config/recent-activity.ts +8 -2
  74. package/src/daemon/dev-cli-debug.ts +10 -1
  75. package/src/detection/ide-detector.ts +26 -16
  76. package/src/git/git-commands.ts +17 -5
  77. package/src/git/git-worktree.ts +8 -1
  78. package/src/index.ts +45 -5
  79. package/src/installer.d.ts +1 -1
  80. package/src/installer.ts +8 -6
  81. package/src/launch.d.ts +1 -1
  82. package/src/launch.ts +37 -28
  83. package/src/logging/async-batch-writer.ts +55 -0
  84. package/src/logging/logger.ts +2 -1
  85. package/src/mesh/beads-db.ts +479 -0
  86. package/src/mesh/contracts.ts +329 -0
  87. package/src/mesh/coordinator-prompt.ts +40 -22
  88. package/src/mesh/coordinator-registry.ts +75 -0
  89. package/src/mesh/mesh-active-work.ts +437 -0
  90. package/src/mesh/mesh-events.ts +799 -56
  91. package/src/mesh/mesh-fast-forward.ts +430 -0
  92. package/src/mesh/mesh-host-ownership.ts +73 -0
  93. package/src/mesh/mesh-ledger.ts +457 -104
  94. package/src/mesh/mesh-refine-status.ts +144 -0
  95. package/src/mesh/mesh-work-queue.ts +216 -158
  96. package/src/mesh/preview-freshness.ts +118 -0
  97. package/src/mesh/refine-config.ts +366 -0
  98. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  99. package/src/providers/approval-utils.ts +39 -5
  100. package/src/providers/chat-message-normalization.ts +7 -12
  101. package/src/providers/cli-provider-instance.ts +362 -41
  102. package/src/providers/contracts.ts +19 -0
  103. package/src/providers/ide-provider-instance.ts +17 -3
  104. package/src/providers/provider-loader.ts +31 -11
  105. package/src/providers/provider-schema.ts +12 -0
  106. package/src/providers/read-chat-contract.ts +76 -16
  107. package/src/providers/transcript-v2.ts +567 -0
  108. package/src/providers/version-archive.ts +38 -20
  109. package/src/repo-mesh-types.ts +77 -0
  110. package/src/shared-types.ts +9 -0
  111. package/src/status/builders.ts +23 -6
  112. package/src/status/reporter.ts +15 -0
  113. package/src/status/snapshot.ts +35 -11
  114. package/src/system/host-memory.ts +29 -12
  115. package/src/types.ts +5 -0
@@ -43,6 +43,20 @@ type ReadChatPayload = {
43
43
  [key: string]: unknown;
44
44
  };
45
45
 
46
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
47
+ let timer: ReturnType<typeof setTimeout> | null = null;
48
+ try {
49
+ return await Promise.race([
50
+ promise,
51
+ new Promise<never>((_, reject) => {
52
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
53
+ }),
54
+ ]);
55
+ } finally {
56
+ if (timer) clearTimeout(timer);
57
+ }
58
+ }
59
+
46
60
  export class IdeProviderInstance implements ProviderInstance {
47
61
  readonly type: string;
48
62
  readonly category = 'ide' as const;
@@ -320,7 +334,7 @@ export class IdeProviderInstance implements ProviderInstance {
320
334
  if (webviewScript) {
321
335
  const matchText = this.provider.webviewMatchText;
322
336
  const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
323
- const webviewRaw = await cdp.evaluateInWebviewFrame(webviewScript, matchFn);
337
+ const webviewRaw = await withTimeout(cdp.evaluateInWebviewFrame(webviewScript, matchFn), 30000, 'evaluateInWebviewFrame');
324
338
  if (webviewRaw) {
325
339
  raw = typeof webviewRaw === 'string' ? (() => { try { return JSON.parse(webviewRaw); } catch { return null; } })() : webviewRaw;
326
340
  }
@@ -331,7 +345,7 @@ export class IdeProviderInstance implements ProviderInstance {
331
345
  if (!raw) {
332
346
  const readChatScript = this.getReadChatScript();
333
347
  if (!readChatScript) return;
334
- raw = await cdp.evaluate(readChatScript, 30000);
348
+ raw = await withTimeout(cdp.evaluate(readChatScript, 30000), 30000, 'evaluate.readChatScript');
335
349
  if (typeof raw === 'string') {
336
350
  try { raw = JSON.parse(raw); } catch { return; }
337
351
  }
@@ -706,7 +720,7 @@ export class IdeProviderInstance implements ProviderInstance {
706
720
  );
707
721
 
708
722
  LOG.info('IdeInstance', `[IdeInstance:${this.type}] autoApprove: executing resolveAction for "${targetButton}"`);
709
- let rawResult = await cdp.evaluate(script, 10000);
723
+ let rawResult = await withTimeout(cdp.evaluate(script, 10000), 10000, 'evaluate.autoApprove');
710
724
  if (typeof rawResult === 'string') {
711
725
  try { rawResult = JSON.parse(rawResult); } catch { }
712
726
  }
@@ -1067,13 +1067,17 @@ export class ProviderLoader {
1067
1067
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
1068
1068
  });
1069
1069
 
1070
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
1070
1071
  const handleChange = (filePath: string) => {
1071
1072
  if (/[\/\\]fixtures[\/\\]/.test(filePath)) {
1072
1073
  return;
1073
1074
  }
1074
1075
  if (filePath.endsWith('.js') || filePath.endsWith('.json')) {
1075
- this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1076
- this.reload();
1076
+ if (reloadTimer) clearTimeout(reloadTimer);
1077
+ reloadTimer = setTimeout(() => {
1078
+ this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1079
+ this.reload();
1080
+ }, 300);
1077
1081
  }
1078
1082
  };
1079
1083
 
@@ -1130,7 +1134,9 @@ export class ProviderLoader {
1130
1134
  return { updated: false };
1131
1135
  }
1132
1136
  const https = require('https') as typeof import('https');
1133
- const { execSync } = require('child_process') as typeof import('child_process');
1137
+ const { exec } = require('child_process') as typeof import('child_process');
1138
+ const { promisify } = require('util');
1139
+ const execAsync = promisify(exec);
1134
1140
 
1135
1141
  const metaPath = path.join(this.upstreamDir, ProviderLoader.META_FILE);
1136
1142
  let prevEtag = '';
@@ -1207,7 +1213,7 @@ export class ProviderLoader {
1207
1213
 
1208
1214
  // Extract
1209
1215
  fs.mkdirSync(tmpExtract, { recursive: true });
1210
- execSync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1216
+ await execAsync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1211
1217
 
1212
1218
  // Tarball internal structure: adhdev-providers-main/ide/... → strip 1 level
1213
1219
  const extracted = fs.readdirSync(tmpExtract);
@@ -1372,11 +1378,15 @@ export class ProviderLoader {
1372
1378
  return args ? args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(' ') : '';
1373
1379
  }
1374
1380
  const schemaDef = this.getSettingsSchema(providerType)[key];
1375
- const defaultVal = schemaDef
1376
- ? (key === 'autoApprove' && schemaDef.type === 'boolean'
1377
- ? true
1378
- : schemaDef.default)
1379
- : undefined;
1381
+ // (fix) Previously this hard-coded `autoApprove` boolean default to `true`,
1382
+ // overriding whatever schemaDef.default the provider.json declared. That
1383
+ // surfaced as soon as a provider added an `autoApprove` schema entry with
1384
+ // default=false: the user had never opted in but the daemon treated the
1385
+ // session as auto-approve, which then triggered recordAutoApproval every
1386
+ // time the CLI showed an approval modal — producing a flood of system
1387
+ // "Auto-approved: ..." messages and keeping the session pinned to
1388
+ // generating while modals cycled in and out. Trust the schemaDef.default.
1389
+ const defaultVal = schemaDef ? schemaDef.default : undefined;
1380
1390
 
1381
1391
  const config = this.readConfig();
1382
1392
  const userVal = config?.providerSettings?.[providerType]?.[key];
@@ -1490,10 +1500,17 @@ export class ProviderLoader {
1490
1500
  ...this.getSyntheticSettings(type, provider),
1491
1501
  ...(provider.settings || {}),
1492
1502
  };
1503
+ // (fix) Previously this clause forced `autoApprove.default = true` for any
1504
+ // boolean autoApprove schema, even when the provider.json explicitly set
1505
+ // `default: false`. Combined with the synthetic-settings fallback at
1506
+ // getSyntheticSettings (which also defaults autoApprove to true when the
1507
+ // provider doesn't supply one), that meant CLI providers silently turned on
1508
+ // auto-approval, producing a flood of "Auto-approved: ..." system messages
1509
+ // every time an approval modal appeared and pinning the session to
1510
+ // generating while modals cycled. Trust the provider's declared default.
1493
1511
  if (result.autoApprove?.type === 'boolean') {
1494
1512
  result.autoApprove = {
1495
1513
  ...result.autoApprove,
1496
- default: true,
1497
1514
  public: true,
1498
1515
  label: result.autoApprove.label || 'Auto Approve',
1499
1516
  description: result.autoApprove.description || 'Automatically approve actionable prompts without sending approval alerts.',
@@ -1518,7 +1535,10 @@ export class ProviderLoader {
1518
1535
  if (!provider.settings?.autoApprove) {
1519
1536
  result.autoApprove = {
1520
1537
  type: 'boolean',
1521
- default: true,
1538
+ // (fix) Safe default is *off*. Auto-approving every modal without the
1539
+ // user opting in produced silent-bash-execution surprises and the
1540
+ // "Auto-approved: ..." system-message flood seen on AGY/Codex.
1541
+ default: false,
1522
1542
  public: true,
1523
1543
  label: 'Auto Approve',
1524
1544
  description: 'Automatically approve actionable prompts without sending approval alerts.',
@@ -64,6 +64,8 @@ const KNOWN_PROVIDER_FIELDS = new Set<string>([
64
64
  'sendKey',
65
65
  'submitStrategy',
66
66
  'requirePromptEchoBeforeSubmit',
67
+ 'requiresFinalAssistantBeforeIdle',
68
+ 'augmentStaleSnapshot',
67
69
  'timeouts',
68
70
  'disableUpstream',
69
71
  ])
@@ -251,6 +253,16 @@ function validateCanonicalHistory(raw: unknown, errors: string[]): void {
251
253
  errors.push('canonicalHistory.mode must be one of: native-source, materialized-mirror, disabled')
252
254
  }
253
255
 
256
+ // Chat transcript contract version (transcript-v2.ts). Absent → treated as
257
+ // v1 by readDeclaredChatContractVersion(). A2 will reject unrecognised
258
+ // values at load time; A1 only validates the field shape.
259
+ const chatContractVersion = canonicalHistory.contractVersion
260
+ if (chatContractVersion !== undefined
261
+ && chatContractVersion !== '1.0'
262
+ && chatContractVersion !== '2.0') {
263
+ errors.push(`canonicalHistory.contractVersion must be '1.0' or '2.0' when provided (got ${JSON.stringify(chatContractVersion)})`)
264
+ }
265
+
254
266
  const scripts = canonicalHistory.scripts
255
267
  if (scripts === undefined) return
256
268
  if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
@@ -1,8 +1,16 @@
1
1
  import type { MessagePart, ModalInfo, ReadChatResult } from './contracts.js'
2
2
  import { normalizeMessageParts } from './contracts.js'
3
3
  import type { ChatBubbleState, ChatMessage } from '../types.js'
4
-
5
- const VALID_STATUSES = ['idle', 'generating', 'waiting_approval', 'error', 'panel_hidden', 'streaming', 'long_generating'] as const
4
+ import {
5
+ CHAT_CONTRACT_VERSION_V1,
6
+ CHAT_CONTRACT_VERSION_V2,
7
+ assertReadChatResultV2Payload,
8
+ isSupportedChatContractVersion,
9
+ type ChatContractVersion,
10
+ type ReadChatResultV2,
11
+ } from './transcript-v2.js'
12
+
13
+ const VALID_STATUSES = ['idle', 'generating', 'waiting_approval', 'error', 'panel_hidden', 'starting', 'streaming', 'long_generating'] as const
6
14
  const VALID_ROLES = ['user', 'assistant', 'system', 'human'] as const
7
15
  const VALID_BUBBLE_STATES = ['draft', 'streaming', 'final', 'removed'] as const
8
16
  const VALID_TURN_STATUSES = ['open', 'waiting_approval', 'complete', 'error'] as const
@@ -63,7 +71,7 @@ function validateMessage(message: unknown, source: string, index: number): ChatM
63
71
  content: validateMessageContent(message.content, source, index),
64
72
  }
65
73
 
66
- if (typeof message.kind === 'string') normalized.kind = message.kind as any
74
+ if (typeof message.kind === 'string') normalized.kind = message.kind as ChatMessage['kind']
67
75
  if (typeof message.id === 'string') normalized.id = message.id
68
76
  if (typeof message.bubbleId === 'string') normalized.bubbleId = message.bubbleId
69
77
  if (typeof message.providerUnitKey === 'string') normalized.providerUnitKey = message.providerUnitKey
@@ -71,20 +79,23 @@ function validateMessage(message: unknown, source: string, index: number): ChatM
71
79
  if (isFiniteNumber(message.index)) normalized.index = message.index
72
80
  if (isFiniteNumber(message.timestamp)) normalized.timestamp = message.timestamp
73
81
  if (isFiniteNumber(message.receivedAt)) normalized.receivedAt = message.receivedAt
74
- if (typeof (message as any)._turnKey === 'string') normalized._turnKey = (message as any)._turnKey
75
- if (Array.isArray(message.toolCalls)) normalized.toolCalls = message.toolCalls as any
76
- if (isPlainObject(message.meta)) normalized.meta = message.meta as any
82
+ // (A2.3) sequence is the monotonic ordering key consumed by ChatSourceMachine.
83
+ // v1 producers omit it; the daemon derives it in normalizeNativeHistoryMessages.
84
+ if (isFiniteNumber(message.sequence)) normalized.sequence = message.sequence
85
+ if (typeof message._turnKey === 'string') normalized._turnKey = message._turnKey
86
+ if (Array.isArray(message.toolCalls)) normalized.toolCalls = message.toolCalls as ChatMessage['toolCalls']
87
+ if (isPlainObject(message.meta)) normalized.meta = message.meta as ChatMessage['meta']
77
88
  if (typeof message.senderName === 'string') normalized.senderName = message.senderName
78
- if (typeof (message as any)._type === 'string') normalized._type = (message as any)._type
79
- if (typeof (message as any)._sub === 'string') normalized._sub = (message as any)._sub
80
- if (typeof (message as any).visibility === 'string') normalized.visibility = (message as any).visibility
81
- if (typeof (message as any).transcriptVisibility === 'string') normalized.transcriptVisibility = (message as any).transcriptVisibility
82
- if (typeof (message as any).audience === 'string') normalized.audience = (message as any).audience
83
- if (typeof (message as any).source === 'string') normalized.source = (message as any).source
84
- if (typeof (message as any).userFacing === 'boolean') normalized.userFacing = (message as any).userFacing
85
- if (typeof (message as any).internal === 'boolean') normalized.internal = (message as any).internal
86
- if (typeof (message as any).isInternal === 'boolean') normalized.isInternal = (message as any).isInternal
87
- if (typeof (message as any).debug === 'boolean') normalized.debug = (message as any).debug
89
+ if (typeof message._type === 'string') normalized._type = message._type
90
+ if (typeof message._sub === 'string') normalized._sub = message._sub
91
+ if (typeof message.visibility === 'string') normalized.visibility = message.visibility
92
+ if (typeof message.transcriptVisibility === 'string') normalized.transcriptVisibility = message.transcriptVisibility
93
+ if (typeof message.audience === 'string') normalized.audience = message.audience
94
+ if (typeof message.source === 'string') normalized.source = message.source
95
+ if (typeof message.userFacing === 'boolean') normalized.userFacing = message.userFacing
96
+ if (typeof message.internal === 'boolean') normalized.internal = message.internal
97
+ if (typeof message.isInternal === 'boolean') normalized.isInternal = message.isInternal
98
+ if (typeof message.debug === 'boolean') normalized.debug = message.debug
88
99
 
89
100
  return normalized
90
101
  }
@@ -129,6 +140,55 @@ function validateControlValues(controlValues: unknown, source: string): Record<s
129
140
  return normalized
130
141
  }
131
142
 
143
+ /**
144
+ * Read the producer-declared contract version from a raw read_chat payload.
145
+ * Returns v1 when absent or unrecognised, so legacy producers keep working
146
+ * through A1. A2 will tighten this to throw when an unsupported version is
147
+ * declared.
148
+ */
149
+ export function readPayloadContractVersion(raw: unknown): ChatContractVersion {
150
+ if (!isPlainObject(raw)) return CHAT_CONTRACT_VERSION_V1
151
+ const declared = (raw as Record<string, unknown>).contractVersion
152
+ if (isSupportedChatContractVersion(declared)) return declared
153
+ return CHAT_CONTRACT_VERSION_V1
154
+ }
155
+
156
+ /**
157
+ * Validate a v2 payload. Thin wrapper around assertReadChatResultV2Payload
158
+ * that prefixes the contract violation with the caller's source label.
159
+ */
160
+ export function validateReadChatResultV2Payload(raw: unknown, source = 'read_chat'): ReadChatResultV2 {
161
+ try {
162
+ return assertReadChatResultV2Payload(raw)
163
+ } catch (err) {
164
+ if (err instanceof Error) {
165
+ err.message = `${source}: ${err.message}`
166
+ }
167
+ throw err
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Versioned entry point. Routes on the producer-declared contractVersion:
173
+ * - v2 → strict v2 validation (transcript-v2.ts invariants)
174
+ * - v1 (or absent) → legacy permissive validation
175
+ *
176
+ * Callers that have not yet been audited to handle v2 outputs should use
177
+ * validateReadChatResultPayload directly; that path stays bound to v1 shape
178
+ * during the A1 transition.
179
+ */
180
+ export function validateReadChatResultPayloadVersioned(
181
+ raw: unknown,
182
+ source = 'read_chat',
183
+ ): { version: typeof CHAT_CONTRACT_VERSION_V1; payload: ReadChatResult & Record<string, unknown> }
184
+ | { version: typeof CHAT_CONTRACT_VERSION_V2; payload: ReadChatResultV2 } {
185
+ const version = readPayloadContractVersion(raw)
186
+ if (version === CHAT_CONTRACT_VERSION_V2) {
187
+ return { version, payload: validateReadChatResultV2Payload(raw, source) }
188
+ }
189
+ return { version: CHAT_CONTRACT_VERSION_V1, payload: validateReadChatResultPayload(raw, source) }
190
+ }
191
+
132
192
  export function validateReadChatResultPayload(raw: unknown, source = 'read_chat'): ReadChatResult & Record<string, unknown> {
133
193
  if (!isPlainObject(raw)) {
134
194
  throw new Error(`${source}: payload must be an object`)