maka-agent 0.2.2-dev.101.20261026 → 0.2.2-dev.106.20261030

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 (42) hide show
  1. package/THIRD_PARTY_NOTICES.txt +2 -2
  2. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  3. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  4. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +6 -0
  5. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +21 -7
  6. package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +32 -4
  7. package/node_modules/@maka/runtime/dist/model-fetcher.js +7 -0
  8. package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +13 -4
  9. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/assistant.js +3 -0
  10. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/controller.js +27 -2
  11. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/notebook.js +73 -7
  12. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/prompt.js +2 -2
  13. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/quick.js +16 -0
  14. package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/controller.js +26 -0
  15. package/node_modules/@maka/runtime-host/dist/assistant/run-cost.js +189 -0
  16. package/node_modules/@maka/runtime-host/dist/assistant/service.js +4 -0
  17. package/node_modules/@maka/runtime-host/dist/protocol/index.js +2 -1
  18. package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -0
  19. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +2 -0
  20. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +5 -0
  21. package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +2 -0
  22. package/node_modules/@maka/runtime-host/package.json +1 -1
  23. package/node_modules/qs/.editorconfig +1 -1
  24. package/node_modules/qs/.github/THREAT_MODEL.md +4 -2
  25. package/node_modules/qs/CHANGELOG.md +61 -14
  26. package/node_modules/qs/README.md +57 -4
  27. package/node_modules/qs/dist/qs.js +19 -19
  28. package/node_modules/qs/eslint.config.mjs +61 -50
  29. package/node_modules/qs/lib/formats.js +7 -2
  30. package/node_modules/qs/lib/parse.js +105 -8
  31. package/node_modules/qs/lib/stringify.js +9 -7
  32. package/node_modules/qs/lib/utils.js +145 -13
  33. package/node_modules/qs/package.json +8 -8
  34. package/node_modules/systeminformation/lib/filesystem.js +2 -1
  35. package/node_modules/systeminformation/lib/graphics.js +1 -1
  36. package/node_modules/systeminformation/lib/index.js +10 -8
  37. package/node_modules/systeminformation/lib/network.js +29 -20
  38. package/node_modules/systeminformation/lib/processes.js +67 -79
  39. package/node_modules/systeminformation/lib/users.js +1 -1
  40. package/node_modules/systeminformation/lib/util.js +75 -30
  41. package/node_modules/systeminformation/package.json +1 -1
  42. package/package.json +1 -1
@@ -6673,7 +6673,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
6673
6673
 
6674
6674
  ================================================================================
6675
6675
 
6676
- Package: qs@6.16.0
6676
+ Package: qs@6.17.0
6677
6677
  Declared license: BSD-3-Clause
6678
6678
  Selected license: BSD-3-Clause
6679
6679
  Repository: https://github.com/ljharb/qs.git
@@ -7361,7 +7361,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
7361
7361
 
7362
7362
  ================================================================================
7363
7363
 
7364
- Package: systeminformation@5.33.13
7364
+ Package: systeminformation@5.33.18
7365
7365
  Declared license: MIT
7366
7366
  Selected license: MIT
7367
7367
  Repository: git+https://github.com/sebhildebrandt/systeminformation.git
@@ -446,6 +446,7 @@ export function decodeConnectionModel(value) {
446
446
  'description',
447
447
  'apiProtocol',
448
448
  'contextWindow',
449
+ 'codexCompactionHash',
449
450
  'inputLimit',
450
451
  'maxOutputTokens',
451
452
  'knowledgeCutoff',
@@ -492,6 +493,11 @@ export function decodeConnectionModel(value) {
492
493
  : {
493
494
  contextWindow: integerValue(item.contextWindow, 'model context window', 1, Number.MAX_SAFE_INTEGER),
494
495
  }),
496
+ ...(item.codexCompactionHash === undefined
497
+ ? {}
498
+ : {
499
+ codexCompactionHash: nonEmptyStringValue(item.codexCompactionHash, 'Codex compaction hash', 128),
500
+ }),
495
501
  ...(item.inputLimit === undefined
496
502
  ? {}
497
503
  : {
@@ -230,27 +230,41 @@ function mergeTextProviderOptions(current, next, textOffset) {
230
230
  function projectToolModePlan(plan, toolMode, execTool) {
231
231
  if (toolMode === 'direct')
232
232
  return plan;
233
+ const directTools = plan.providerTools.filter((tool) => tool.nesting === 'direct_only');
234
+ const directNames = new Set(directTools.map((tool) => tool.name));
235
+ const projectActive = (names) => [
236
+ execTool.name,
237
+ ...names.filter((name) => directNames.has(name)),
238
+ ];
239
+ const projectBaseActive = plan.projectActiveTools;
233
240
  return {
234
241
  ...plan,
235
242
  providerTools: [
236
243
  execTool,
244
+ ...directTools,
237
245
  ...plan.providerTools.filter((tool) => tool.name === INVALID_TOOL_NAME),
238
246
  ],
239
- activeTools: [execTool.name],
240
- ...(plan.projectActiveTools
241
- ? { projectActiveTools: () => ({ activeTools: [execTool.name] }) }
247
+ activeTools: projectActive(plan.activeTools),
248
+ ...(projectBaseActive
249
+ ? {
250
+ projectActiveTools: (options) => ({
251
+ activeTools: projectActive(projectBaseActive(options).activeTools),
252
+ }),
253
+ }
242
254
  : {}),
243
- currentRepairToolNames: () => [execTool.name],
255
+ currentRepairToolNames: () => projectActive(plan.currentRepairToolNames()),
244
256
  diagnostics: () => undefined,
245
257
  };
246
258
  }
247
- function renderCodeModeCatalogPrompt(nested) {
259
+ function renderCodeModeCatalogPrompt(nested, directNames) {
248
260
  // The aggregate catalog can exceed the evidence codec's bound for one tool
249
261
  // description. Keep exec's schema fixed; requestSystemPrompt and its hash
250
262
  // carry this step's exact, refreshable nested surface instead.
251
263
  const catalog = requestCompositionToolSchemas([...nested.values()], [...nested.keys()]);
252
264
  return [
253
- 'Code Mode: exec is the only callable tool. Call the following tools from inside exec.',
265
+ directNames.length
266
+ ? `Code Mode: call the tools in the following catalog from inside exec. Call these control tools directly, never from exec: ${directNames.join(', ')}.`
267
+ : 'Code Mode: exec is the only callable tool. Call the following tools from inside exec.',
254
268
  'After tool_search, return its result and use the refreshed catalog in the next exec call.',
255
269
  JSON.stringify(catalog),
256
270
  ].join('\n');
@@ -1196,7 +1210,7 @@ export class AiSdkTurn {
1196
1210
  const codeModeCatalogPrompt = toolMode === 'code_mode'
1197
1211
  ? renderCodeModeCatalogPrompt(toolRuntime.hasSandboxBoundaryDenial()
1198
1212
  ? new Map([...nestedTools].filter(([name]) => name !== REQUEST_SANDBOX_BOUNDARY_TOOL_NAME))
1199
- : nestedTools)
1213
+ : nestedTools, boundaryAwareToolNames(plan.currentRepairToolNames()).filter((name) => name !== codeModeExecTool.name))
1200
1214
  : undefined;
1201
1215
  const resolveDispatch = (active) => {
1202
1216
  const activeTools = finalChildSummaryStep || sandboxBoundaryFinalizationStep
@@ -204,13 +204,36 @@ export function isProviderHistoryCompactCheckpoint(checkpoint) {
204
204
  export function isTextHistoryCompactCheckpoint(checkpoint) {
205
205
  return checkpoint.version === 2;
206
206
  }
207
+ /** Bound the age of provider compatibility claims, including long-lived backends. */
208
+ export const CODEX_COMPACTION_METADATA_MAX_AGE_MS = 5 * 60 * 1000;
209
+ export function freshCodexCompactionHash(connection, modelId, now = Date.now()) {
210
+ const fetchedAt = connection.codexCompactionMetadataFetchedAt;
211
+ if (connection.providerType !== 'openai-codex' ||
212
+ fetchedAt === undefined ||
213
+ !Number.isSafeInteger(fetchedAt) ||
214
+ fetchedAt < 0 ||
215
+ !Number.isFinite(now) ||
216
+ fetchedAt > now ||
217
+ now - fetchedAt >= CODEX_COMPACTION_METADATA_MAX_AGE_MS)
218
+ return undefined;
219
+ return connection.models?.find((model) => model.id === modelId)?.codexCompactionHash;
220
+ }
207
221
  export function canReplayHistoryCompactCheckpointForModel(checkpoint, connection, connectionId, modelId) {
208
222
  if (!isProviderHistoryCompactCheckpoint(checkpoint))
209
223
  return true;
210
- return (connection.providerType === 'openai-codex' &&
211
- connectionId !== undefined &&
212
- checkpoint.providerState.connectionId === connectionId &&
213
- checkpoint.providerState.modelId === modelId);
224
+ const state = checkpoint.providerState;
225
+ if (connection.providerType !== 'openai-codex' ||
226
+ connectionId === undefined ||
227
+ state.connectionId !== connectionId)
228
+ return false;
229
+ const targetHash = freshCodexCompactionHash(connection, modelId);
230
+ // Legacy checkpoints remain usable on their original model. Never infer a
231
+ // legacy checkpoint's original hash from today's mutable model catalog.
232
+ if (state.modelId === modelId && (!state.codexCompactionHash || !targetHash))
233
+ return true;
234
+ return (nonEmpty(state.codexCompactionHash) &&
235
+ nonEmpty(targetHash) &&
236
+ state.codexCompactionHash === targetHash);
214
237
  }
215
238
  /** Whether this model's configured compactor can roll forward from this checkpoint value. */
216
239
  export function canContinueHistoryCompactCheckpointForModel(checkpoint, connection, connectionId, modelId) {
@@ -582,6 +605,11 @@ function validHistoryCompactProviderState(value) {
582
605
  return (state.kind === 'openai_codex_remote_v2' &&
583
606
  nonEmpty(state.connectionId) &&
584
607
  nonEmpty(state.modelId) &&
608
+ (state.codexCompactionHash === undefined ||
609
+ (nonEmpty(state.codexCompactionHash) &&
610
+ state.codexCompactionHash.length <= 128 &&
611
+ state.codexCompactionHash.trim() === state.codexCompactionHash &&
612
+ !/[\u0000-\u001f\u007f]/.test(state.codexCompactionHash))) &&
585
613
  nonEmpty(state.itemId) &&
586
614
  nonEmpty(state.encryptedContent));
587
615
  }
@@ -300,6 +300,13 @@ async function fetchOpenAiCodexModels(baseUrl, accessToken, fetchFn) {
300
300
  const contextWindow = providerTokenLimit(model.context_window);
301
301
  if (contextWindow !== undefined)
302
302
  entry.contextWindow = contextWindow;
303
+ if (typeof model.comp_hash === 'string' &&
304
+ model.comp_hash.length > 0 &&
305
+ model.comp_hash.length <= 128 &&
306
+ model.comp_hash.trim() === model.comp_hash &&
307
+ !/[\u0000-\u001f\u007f]/.test(model.comp_hash)) {
308
+ entry.codexCompactionHash = model.comp_hash;
309
+ }
303
310
  return entry;
304
311
  });
305
312
  }
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { HistoryCompactSummarizerError } from './history-compact-error.js';
20
20
  import { nonEmpty } from './context-budget-helpers.js';
21
- import { canContinueHistoryCompactCheckpointForModel, historyCompactCheckpointToModelMessage, isProviderHistoryCompactCheckpoint, projectHistoryCompactCheckpointReplay, } from './history-compact-checkpoint.js';
21
+ import { canContinueHistoryCompactCheckpointForModel, freshCodexCompactionHash, historyCompactCheckpointToModelMessage, isProviderHistoryCompactCheckpoint, projectHistoryCompactCheckpointReplay, } from './history-compact-checkpoint.js';
22
22
  import { admitProviderReasoningReplayItems, buildRuntimeEventModelReplayPlan, buildRuntimeEventReplayTimeline, compatibleProviderReasoningReplayEventIds, } from './model-history.js';
23
23
  import { withProviderStreamTracking } from './provider-request-telemetry.js';
24
24
  import { effectiveReplayToolResultOutput } from './durable-tool-result-projection.js';
@@ -40,7 +40,11 @@ export function buildOpenAiCodexHistoryCompactor(options) {
40
40
  try {
41
41
  const previous = input.previousCheckpoint;
42
42
  const canContinuePrevious = previous &&
43
- canContinueHistoryCompactCheckpointForModel(previous, { providerType: 'openai-codex' }, options.connectionId, options.modelId);
43
+ canContinueHistoryCompactCheckpointForModel(previous, {
44
+ providerType: 'openai-codex',
45
+ models: options.models,
46
+ codexCompactionMetadataFetchedAt: options.codexCompactionMetadataFetchedAt,
47
+ }, options.connectionId, options.modelId);
44
48
  const events = canContinuePrevious
45
49
  ? (input.newlyFoldedRuntimeEvents ?? [])
46
50
  : input.source.foldedRuntimeEvents;
@@ -92,7 +96,11 @@ export function buildOpenAiCodexHistoryCompactor(options) {
92
96
  }
93
97
  if (streamError)
94
98
  throw streamError;
95
- const state = extractOpenAiCodexCompactionState(content, options.connectionId, options.modelId);
99
+ const state = extractOpenAiCodexCompactionState(content, options.connectionId, options.modelId, freshCodexCompactionHash({
100
+ providerType: 'openai-codex',
101
+ models: options.models,
102
+ codexCompactionMetadataFetchedAt: options.codexCompactionMetadataFetchedAt,
103
+ }, options.modelId));
96
104
  if (!state)
97
105
  throw new HistoryCompactSummarizerError('invalid_provider_state');
98
106
  return state;
@@ -256,7 +264,7 @@ function openAiCodexCompactionMessages(events, providerReasoningReplayEventIds)
256
264
  }
257
265
  return messages;
258
266
  }
259
- export function extractOpenAiCodexCompactionState(content, connectionId, modelId) {
267
+ export function extractOpenAiCodexCompactionState(content, connectionId, modelId, codexCompactionHash) {
260
268
  const parts = (content ?? []).filter(isOpenAiCompactionPart);
261
269
  if (parts.length !== 1)
262
270
  return undefined;
@@ -267,6 +275,7 @@ export function extractOpenAiCodexCompactionState(content, connectionId, modelId
267
275
  kind: 'openai_codex_remote_v2',
268
276
  connectionId,
269
277
  modelId,
278
+ ...(codexCompactionHash ? { codexCompactionHash } : {}),
270
279
  itemId: metadata.itemId,
271
280
  encryptedContent: metadata.encryptedContent,
272
281
  };
@@ -92,6 +92,9 @@ export function registerAssistant(ctx, store, controller) {
92
92
  },
93
93
  ...(includeMemory ? { memory: memoryState } : {}),
94
94
  tasks: taskState,
95
+ // What background discovery and Matter activations cost and whether they reported
96
+ // (#6309). Observability only: nothing in the scheduler reads it.
97
+ cost: ctx.runCost?.summary() ?? null,
95
98
  hostRequired: true,
96
99
  };
97
100
  };
@@ -315,9 +315,23 @@ export class InitiativeController {
315
315
  }
316
316
  async run(agent, state) {
317
317
  const id = state.active.id;
318
+ const startedAt = state.active.startedAt ?? Date.now();
319
+ // Each Session the heartbeat drives and the window it was driven in, so the
320
+ // run's cost can be read back from the Usage ledger afterwards (#6309).
321
+ const segments = [];
322
+ const measured = async (worker, work) => {
323
+ const from = Date.now();
324
+ try {
325
+ await work();
326
+ }
327
+ finally {
328
+ segments.push({ sessionId: worker.sessionId, from, to: Date.now() });
329
+ }
330
+ };
331
+ let outcome = 'failed';
318
332
  this.activeAgent = agent;
319
333
  try {
320
- await this.execute(agent, state);
334
+ await measured(agent, () => this.execute(agent, state));
321
335
  if (state.active.phase === 'discovery') {
322
336
  const next = this.store.update((s) => {
323
337
  if (s.active?.id !== id)
@@ -330,8 +344,10 @@ export class InitiativeController {
330
344
  const snapshot = await judge.snapshot();
331
345
  if (snapshot?.agent?.status === 'running' || awaitingUser(snapshot))
332
346
  throw Error('Judgement worker is busy or awaiting user input; notebook retained');
333
- await this.execute(judge, this.store.get());
347
+ await measured(judge, () => this.execute(judge, this.store.get()));
334
348
  }
349
+ // The signal the run's `lastRun` outcome is derived from: the Bot accepted a report.
350
+ outcome = this.store.get()?.active?.reported ? 'reported' : 'no_report';
335
351
  this.store.finish(id);
336
352
  }
337
353
  catch (e) {
@@ -352,6 +368,15 @@ export class InitiativeController {
352
368
  }
353
369
  finally {
354
370
  this.activeAgent = undefined;
371
+ // Observation only: the ledger swallows its own failures and nothing reads it back.
372
+ await this.ctx.runCost?.record({
373
+ kind: 'discovery',
374
+ runId: id,
375
+ startedAt,
376
+ endedAt: Date.now(),
377
+ outcome,
378
+ segments,
379
+ });
355
380
  }
356
381
  }
357
382
  async enable(input, call) {
@@ -110,21 +110,44 @@ export class Notebook {
110
110
  requireThat(topic, 'Unknown topic');
111
111
  let current = { status: 'open', reason: null, feedbackId: null };
112
112
  for (const e of this.feedbackResolutions().sort((a, b) => this.event(a.payload.feedbackId).seq - this.event(b.payload.feedbackId).seq))
113
- for (const rule of e.payload.rules) {
113
+ for (const [ruleIndex, rule] of e.payload.rules.entries()) {
114
114
  if (!rule.topicIds.includes(topicId) && !overlaps(rule.tags, topic.tags))
115
115
  continue;
116
- current = { ...rule, feedbackId: e.payload.feedbackId };
116
+ current = { ...rule, feedbackId: e.payload.feedbackId, ruleIndex };
117
117
  }
118
+ if (current.feedbackId && ['closed', 'deferred'].includes(current.status)) {
119
+ const governing = this.event(current.feedbackId).seq;
120
+ // A reopening answers one specific rule. Topics gain tags over time, so the
121
+ // rule that governs a topic can change with no new feedback event: a later
122
+ // rule may start to match. Apply only a reopening the model judged against
123
+ // the rule that governs now; a different rule was never judged.
124
+ const reopen = this.events('reopen')
125
+ .filter((e) => e.payload.topicId === topicId &&
126
+ e.seq > governing &&
127
+ e.payload.feedbackId === current.feedbackId &&
128
+ e.payload.ruleIndex === current.ruleIndex)
129
+ .at(-1);
130
+ if (reopen)
131
+ current = {
132
+ status: 'open',
133
+ reason: reopen.payload.reason,
134
+ feedbackId: current.feedbackId,
135
+ ruleIndex: current.ruleIndex,
136
+ reopenedByChange: true,
137
+ };
138
+ }
118
139
  if (current.status === 'deferred' &&
119
140
  current.resumeAt &&
120
141
  Date.parse(now) >= Date.parse(current.resumeAt))
121
142
  current = { ...current, status: 'open', resumedByUserDeadline: true };
122
143
  const receipts = this.events('delivery').filter((e) => e.payload.topicIds.includes(topicId));
144
+ const withheld = this.events('withheld').filter((e) => e.payload.topicId === topicId);
123
145
  // Display 'sent' without destroying the user's open/deferred/closed policy.
124
146
  return {
125
147
  ...current,
126
148
  displayStatus: current.status === 'open' && receipts.length ? 'sent' : current.status,
127
149
  receipts,
150
+ withheld,
128
151
  };
129
152
  }
130
153
  recordFeedback({ id, text, at = this.clock(), replyTo = null, }) {
@@ -168,6 +191,42 @@ export class Notebook {
168
191
  }
169
192
  return this.put(id, 'feedback-resolution', resolution);
170
193
  }
194
+ /**
195
+ * The judgement looked into a topic and chose not to tell the user. This is
196
+ * neither "investigated" (notes) nor "told" (delivery receipts), and it is not
197
+ * a ban: a later message about the topic is still allowed.
198
+ */
199
+ recordWithheld(entry, id) {
200
+ requireThat(entry &&
201
+ Object.keys(entry).every((k) => ['topicId', 'reason'].includes(k)) &&
202
+ nonempty(entry.topicId) &&
203
+ nonempty(entry.reason), 'Withheld needs a topic and a reason');
204
+ requireThat(this.topics().has(entry.topicId), 'Unknown withheld topic');
205
+ return this.put(id, 'withheld', entry);
206
+ }
207
+ /**
208
+ * A closed or deferred topic becomes open again because its underlying
209
+ * information changed, not because the user said so. The change must be in the
210
+ * notebook: a note on this topic recorded after the user feedback that governs
211
+ * it. The user's own later feedback still overrides this.
212
+ */
213
+ reopenTopic(entry, id) {
214
+ requireThat(entry &&
215
+ Object.keys(entry).every((k) => ['topicId', 'reason'].includes(k)) &&
216
+ nonempty(entry.topicId) &&
217
+ nonempty(entry.reason), 'Reopening needs a topic and a reason');
218
+ requireThat(this.topics().has(entry.topicId), 'Unknown reopened topic');
219
+ const current = this.state(entry.topicId);
220
+ requireThat(current.feedbackId && ['closed', 'deferred'].includes(current.status), 'Only a topic the user closed or deferred can be reopened');
221
+ const governing = this.event(current.feedbackId).seq;
222
+ requireThat(this.events('note').some((e) => e.payload.topicId === entry.topicId && e.seq > governing), 'Reopening needs a note on this topic recorded after the user feedback');
223
+ // Bind the reopening to the rule it was validated against (see state()).
224
+ return this.put(id, 'reopen', {
225
+ ...entry,
226
+ feedbackId: current.feedbackId,
227
+ ruleIndex: current.ruleIndex,
228
+ });
229
+ }
171
230
  commitDraft({ role, operationId, expectedFeedbackRevision, draft, }) {
172
231
  requireThat(['exploration', 'judgement'].includes(role), 'Unknown role');
173
232
  const digest = fingerprint({ role, expectedFeedbackRevision, draft });
@@ -179,14 +238,17 @@ export class Notebook {
179
238
  }
180
239
  requireThat(this.revision() === expectedFeedbackRevision, 'FEEDBACK_CHANGED: refresh feedback before committing');
181
240
  requireThat(draft &&
182
- Object.keys(draft).every((k) => ['notes', 'feedbackDecisions', 'messages'].includes(k)), 'Unsupported draft field');
183
- const { notes = [], feedbackDecisions = [], messages = [] } = draft;
184
- requireThat([notes, feedbackDecisions, messages].every(Array.isArray), 'Draft operations must be arrays');
185
- requireThat(role === 'judgement' || (!feedbackDecisions.length && !messages.length), 'Exploration only appends knowledge');
241
+ Object.keys(draft).every((k) => ['notes', 'feedbackDecisions', 'messages', 'withheld', 'reopened'].includes(k)), 'Unsupported draft field');
242
+ const { notes = [], feedbackDecisions = [], messages = [], withheld = [], reopened = [], } = draft;
243
+ requireThat([notes, feedbackDecisions, messages, withheld, reopened].every(Array.isArray), 'Draft operations must be arrays');
244
+ requireThat(role === 'judgement' ||
245
+ (!feedbackDecisions.length && !messages.length && !withheld.length && !reopened.length), 'Exploration only appends knowledge');
186
246
  notes.forEach((n, i) => this.appendNote(n, `${operationId}:note:${i}`));
187
247
  feedbackDecisions.forEach((f, i) => this.resolveFeedback(f, `${operationId}:feedback:${i}`));
188
248
  if (role === 'judgement')
189
249
  requireThat(this.pendingFeedback().length === 0, 'Read and interpret pending user feedback first');
250
+ withheld.forEach((w, i) => this.recordWithheld(w, `${operationId}:withheld:${i}`));
251
+ reopened.forEach((r, i) => this.reopenTopic(r, `${operationId}:reopened:${i}`));
190
252
  const preparedIds = [];
191
253
  messages.forEach((m, i) => {
192
254
  requireThat(Object.keys(m).every((k) => ['text', 'topicIds'].includes(k)) &&
@@ -204,6 +266,8 @@ export class Notebook {
204
266
  preparedIds,
205
267
  notesAdded: notes.length,
206
268
  feedbackProcessed: feedbackDecisions.length,
269
+ withheldRecorded: withheld.length,
270
+ topicsReopened: reopened.length,
207
271
  };
208
272
  this.put(operationId, 'draft', { digest, result });
209
273
  return result;
@@ -270,13 +334,15 @@ export class Notebook {
270
334
  deferred: '暂缓',
271
335
  closed: '关闭',
272
336
  };
273
- lines.push('', `## ${topic.title}〔${topic.id}〕`, `状态:${labels[s.displayStatus]}${s.reason ? ';原因:' + s.reason : ''}${s.feedbackId ? ';依据:' + s.feedbackId : ''}`, `方向:${topic.tags.join('、') || '未分类'}`);
337
+ lines.push('', `## ${topic.title}〔${topic.id}〕`, `状态:${labels[s.displayStatus]}${s.reopenedByChange ? '(因信息出现重大变化,经判断重新开放)' : ''}${s.reason ? ';原因:' + s.reason : ''}${s.feedbackId ? ';依据:' + s.feedbackId : ''}`, `方向:${topic.tags.join('、') || '未分类'}`);
274
338
  if (s.resumeAt)
275
339
  lines.push(`用户指定恢复时间:${s.resumeAt}`);
276
340
  for (const n of topic.notes)
277
341
  lines.push(`- ${n.text}\n 发生时间:${n.occurredAt ?? '未知'}|记录时间:${n.recordedAt}\n 来源:${n.sources.join(';')}${n.related?.length ? '\n 关联主题:' + n.related.join('、') : ''}`);
278
342
  for (const r of s.receipts)
279
343
  lines.push(`- 已发送 ${r.at}〔${r.id}〕:${r.payload.text}`);
344
+ for (const w of s.withheld)
345
+ lines.push(`- 已调查但未告知用户 ${w.at}〔${w.id}〕:${w.payload.reason}`);
280
346
  }
281
347
  const quickDeliveries = this.events('delivery').filter((e) => e.payload.origin === 'quick-discovery' && !e.payload.topicIds.length);
282
348
  if (quickDeliveries.length) {
@@ -46,8 +46,8 @@ export function wakeInput({ role, now, viewPath, outputPath, first = false, }) {
46
46
  if (!['exploration', 'judgement'].includes(role))
47
47
  throw Error('Unknown role');
48
48
  const contract = `${role === 'judgement' ? '基于用户的全局信息和整份记事本自主调查并判断,不只看新内容或本轮增量。' : '基于用户的全局信息探索,用工具读取笔记并按需自主调查,不只看新内容或本轮增量。'}完成后把增量操作写入 ${outputPath},格式:
49
- {"notes":[{"topicId":"已有主题ID或新ID","title":"主题名","text":"简短认识","occurredAt":null,"sources":["信息来源"],"tags":["方向标签"],"related":["可选的相关主题ID"]}],"feedbackDecisions":[{"feedbackId":"用户反馈ID","understanding":"对反馈的理解","rules":[{"quote":"反馈原话中的依据","status":"open|deferred|closed","reason":"原因","resumeAt":null,"topicIds":["作用主题"],"tags":["作用方向"]}]}],"messages":[{"text":"完整消息","topicIds":["涉及主题"]}]}。
50
- occurredAt可写已知的发生时间或范围,不知道就null。无需填满各数组;输出只包含本次增量,不复制整本笔记。主题和方向标签可按内容新增,相关用户反馈仍需考虑。${role === 'exploration' ? '只填写notes,另外两个数组留空。' : '没有待整理反馈时feedbackDecisions留空;只有用户明确表达状态变化才写rules,否则记理解和有价值的信息即可。关闭方向可能涉及多个主题,方向标签可同时作用于以后相关主题。'}
49
+ {"notes":[{"topicId":"已有主题ID或新ID","title":"主题名","text":"简短认识","occurredAt":null,"sources":["信息来源"],"tags":["方向标签"],"related":["可选的相关主题ID"]}],"feedbackDecisions":[{"feedbackId":"用户反馈ID","understanding":"对反馈的理解","rules":[{"quote":"反馈原话中的依据","status":"open|deferred|closed","reason":"原因","resumeAt":null,"topicIds":["作用主题"],"tags":["作用方向"]}]}],"messages":[{"text":"完整消息","topicIds":["涉及主题"]}],"withheld":[{"topicId":"已调查但决定不告知用户的主题ID","reason":"不告知的原因"}],"reopened":[{"topicId":"用户已关闭或暂缓的主题ID","reason":"信息出现了什么重大变化"}]}。
50
+ occurredAt可写已知的发生时间或范围,不知道就null。无需填满各数组;输出只包含本次增量,不复制整本笔记。主题和方向标签可按内容新增,相关用户反馈仍需考虑。${role === 'exploration' ? '只填写notes,其余数组留空。' : '没有待整理反馈时feedbackDecisions留空;只有用户明确表达状态变化才写rules,否则记理解和有价值的信息即可。关闭方向可能涉及多个主题,方向标签可同时作用于以后相关主题。调查过某主题但决定不告知时写withheld,与已告知分开记录;只有当用户已关闭或暂缓的主题出现重大变化(先把变化按记事本规则追加为该主题的新笔记)时才写reopened,没有重大变化就保持用户的决定。'}
51
51
  提交与送达以当前记事本记录为准,写过结果文件不代表已提交或已发送。最终回复结果文件路径。`;
52
52
  return `${first ? (role === 'exploration' ? DISCOVERY_TASK : JUDGEMENT_TASK) + '\n\n' + NOTEBOOK_RULES + '\n\n' : ''}当前 UTC 时间:${now}\n记事本:${viewPath}\n${contract}`;
53
53
  }
@@ -350,9 +350,14 @@ export class QuickDiscoveryController {
350
350
  }
351
351
  async run(agent, state) {
352
352
  const active = state.active;
353
+ // Observation only (#6309): the model window of this attempt, when it ran one.
354
+ const startedAt = Date.now();
355
+ let modelFrom;
356
+ let outcome = 'failed';
353
357
  try {
354
358
  this.forward();
355
359
  if (!active.executionCompleted) {
360
+ modelFrom = Date.now();
356
361
  this.parent.refreshNotebook();
357
362
  const version = hash(QUICK_DISCOVERY_TASK);
358
363
  const input = quickWakeInput(active.from, active.to, this.state().promptVersion !== version);
@@ -381,6 +386,7 @@ export class QuickDiscoveryController {
381
386
  });
382
387
  }
383
388
  this.forward();
389
+ outcome = this.state()?.active?.reports?.length ? 'reported' : 'no_report';
384
390
  this.store.update((s) => {
385
391
  if (s.active?.id !== active.id)
386
392
  throw Error('Quick wake ownership changed');
@@ -410,6 +416,16 @@ export class QuickDiscoveryController {
410
416
  }
411
417
  finally {
412
418
  this.agent = undefined;
419
+ // A retry that only redelivers an earlier report ran no model: not a run.
420
+ if (modelFrom !== undefined)
421
+ await this.ctx.runCost?.record({
422
+ kind: 'quick_discovery',
423
+ runId: active.id,
424
+ startedAt,
425
+ endedAt: Date.now(),
426
+ outcome,
427
+ segments: [{ sessionId: agent.sessionId, from: modelFrom, to: Date.now() }],
428
+ });
413
429
  }
414
430
  }
415
431
  async close() {
@@ -141,11 +141,16 @@ export class MatterController {
141
141
  const activationId = m.activation.id;
142
142
  let queued = false;
143
143
  let failureKind;
144
+ // Observation only (#6309): undefined means no model execution was admitted.
145
+ const startedAt = m.activation.startedAt;
146
+ let outcome = 'failed';
147
+ let retried = false;
144
148
  try {
145
149
  if (!alreadyStarted) {
146
150
  const result = await agent.followup(buildMatterPrompt(this.store.workspace(m.id, activationId)));
147
151
  if (result.disposition === 'blocked') {
148
152
  this.store.deferAdmission(m.id, activationId, 'Host 暂未接收唤醒,将自动重试。');
153
+ outcome = undefined;
149
154
  return;
150
155
  }
151
156
  if (result.disposition === 'turn_started') {
@@ -180,6 +185,8 @@ export class MatterController {
180
185
  failureKind = failure?.kind;
181
186
  if (failure?.retryable) {
182
187
  this.store.retryExecution(m.id, activationId, failure);
188
+ outcome = 'retried';
189
+ retried = true;
183
190
  return;
184
191
  }
185
192
  throw Error(failure?.message ??
@@ -195,6 +202,11 @@ export class MatterController {
195
202
  await completion();
196
203
  if (isPendingActivationTurnId(this.store.get(m.id).matter.activation?.turnId))
197
204
  throw new Error('Queued wake ended without claiming its activation; inspect conversation before resuming');
205
+ const live = this.store.get(m.id).matter.activation;
206
+ // A retry already decided the outcome: it cleared the activation on purpose,
207
+ // so do not read that as the user interrupting the run.
208
+ if (!retried)
209
+ outcome = live?.id !== activationId ? 'interrupted' : live.settled ? 'completed' : 'failed';
198
210
  this.store.finish(m.id, activationId);
199
211
  }
200
212
  catch (e) {
@@ -204,6 +216,7 @@ export class MatterController {
204
216
  isPendingActivationTurnId(live.activation.turnId) &&
205
217
  /Runtime Host is not ready for Plugin Agent messages/.test(String(e))) {
206
218
  this.store.deferAdmission(m.id, activationId, String(e));
219
+ outcome = undefined;
207
220
  return;
208
221
  }
209
222
  if (live.activation?.id === activationId) {
@@ -212,6 +225,19 @@ export class MatterController {
212
225
  await agent.cancel().catch(() => { });
213
226
  }
214
227
  }
228
+ finally {
229
+ // The ledger swallows its own failures and nothing reads it back.
230
+ if (outcome)
231
+ await this.ctx.runCost?.record({
232
+ kind: 'matter_activation',
233
+ runId: activationId,
234
+ matterId: m.id,
235
+ startedAt,
236
+ endedAt: Date.now(),
237
+ outcome,
238
+ segments: [{ sessionId: m.sessionId, from: startedAt, to: Date.now() }],
239
+ });
240
+ }
215
241
  }
216
242
  async enroll(input, call) {
217
243
  this.assertReady();