mixdog 1.3.3 → 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.
- package/LICENSES/oh-my-pi-MIT.txt +23 -0
- package/NOTICE.md +15 -0
- package/README.md +11 -0
- package/package.json +3 -2
- package/scripts/embed-google-oauth-client.mjs +49 -0
- package/scripts/lib/test-spawn-ledger.mjs +46 -1
- package/scripts/test.mjs +5 -0
- package/src/defaults/shipped-history.json +8 -0
- package/src/defaults/skills/browser-use/SKILL.md +21 -9
- package/src/defaults/skills/setup/SKILL.md +7 -2
- package/src/defaults/skills/setup/references/actions.md +82 -27
- package/src/defaults/skills/setup/references/surfaces.md +67 -34
- package/src/defaults/skills/setup-guide/SKILL.md +3 -3
- package/src/lib/keychain-cjs.cjs +359 -29
- package/src/rules/agent-mode/AGENT_MODE.md +10 -2
- package/src/rules/agent-mode/MIXY.md +12 -0
- package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
- package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
- package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
- package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
- package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
- package/src/runtime/channels/lib/scheduler.mjs +109 -14
- package/src/runtime/channels/lib/schedules-db.mjs +49 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
- package/src/runtime/channels/lib/worker-main.mjs +9 -0
- package/src/runtime/office/com/com-adapter.mjs +17 -1
- package/src/runtime/office/com/office-com-host.ps1 +51 -1
- package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
- package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
- package/src/runtime/office/pdf/document-preview.mjs +326 -8
- package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
- package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
- package/src/runtime/office/portable/portable-soffice.mjs +45 -5
- package/src/runtime/shared/agent-room-host.mjs +1 -1
- package/src/runtime/shared/bot-hops.mjs +62 -2
- package/src/runtime/shared/config.mjs +49 -22
- package/src/runtime/shared/connector-catalog.d.mts +4 -2
- package/src/runtime/shared/connector-catalog.mjs +16 -8
- package/src/runtime/shared/developer-options.mjs +32 -1
- package/src/runtime/shared/google-oauth-client.generated.json +4 -0
- package/src/runtime/shared/google-oauth-client.mjs +34 -9
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
- package/src/runtime/shared/llm/well-formed.mjs +66 -0
- package/src/runtime/shared/provider-accounts.mjs +224 -58
- package/src/runtime/shared/provider-api-key.mjs +19 -8
- package/src/runtime/shared/routine-trigger.mjs +42 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -0
- package/src/runtime/shared/transcript-metadata.mjs +38 -0
- package/src/runtime/shared/user-data-guard.mjs +30 -2
- package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
- package/src/session-runtime/agent-home-delivery.mjs +57 -13
- package/src/session-runtime/agent-room-delivery.mjs +28 -1
- package/src/session-runtime/agent-task-sessions.mjs +292 -79
- package/src/session-runtime/agent-tool-routing.mjs +49 -4
- package/src/session-runtime/boot/begin.mjs +3 -2
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/bot-chain.mjs +32 -0
- package/src/session-runtime/bot-status.mjs +7 -4
- package/src/session-runtime/channel-config-api.mjs +63 -17
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
- package/src/session-runtime/internal-tool-executor.mjs +4 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
- package/src/session-runtime/mixy-heartbeat.mjs +807 -0
- package/src/session-runtime/model-route/route-tuning.mjs +10 -6
- package/src/session-runtime/owned-automation-run.mjs +33 -12
- package/src/session-runtime/provider-auth/login.mjs +7 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
- package/src/session-runtime/routine-home-turn.mjs +33 -6
- package/src/session-runtime/schedule-session-run.mjs +7 -3
- package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
- package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
- package/src/session-runtime/services/agent-tool/render.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
- package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
- package/src/session-runtime/services/channel-admin.mjs +66 -18
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +128 -25
- package/src/session-runtime/services/schedule-access.mjs +14 -0
- package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
- package/src/session-runtime/session-create/session-options.mjs +7 -4
- package/src/session-runtime/settings-system-api.mjs +16 -1
- package/src/session-runtime/setup-tool/executor.mjs +49 -14
- package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
- package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
- package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
- package/src/session-runtime/task-session-route.mjs +99 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
- package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
- package/src/session-runtime/tool-surface.mjs +4 -0
- package/src/session-runtime/usage-stats-api.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
- package/src/session-runtime/workspace/transitions.mjs +10 -2
- package/src/standalone/daemon-session-control.mjs +88 -20
- package/src/standalone/daemon.mjs +8 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
- package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
- package/src/standalone/session-service.mjs +13 -0
- package/src/standalone/session-tool-output.mjs +8 -5
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/dist/index.mjs +335 -172
- package/src/tui/session/notice-surface.mjs +17 -1
- package/src/tui/session/session-api/integrations.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +8 -0
- package/src/tui/session/session-api/settings.mjs +14 -2
- package/src/tui/session/turn-stream.mjs +17 -1
- package/src/tui/session/turn.mjs +22 -2
- package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
- package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
- package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
|
@@ -11,6 +11,7 @@ export const ACCOUNT_PROVIDERS = Object.freeze([
|
|
|
11
11
|
'command-code-oauth',
|
|
12
12
|
'mimo-oauth',
|
|
13
13
|
'cursor-oauth',
|
|
14
|
+
'devin-oauth',
|
|
14
15
|
'antigravity-oauth',
|
|
15
16
|
'opencode-go-oauth',
|
|
16
17
|
]);
|
|
@@ -95,16 +96,32 @@ export function providerAccountPolicy(pool) {
|
|
|
95
96
|
return pool?.policy === 'headroom' ? 'headroom' : 'limit';
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
// A reading stored before per-model gating is stale: it is dropped on read so
|
|
100
|
+
// it steers nothing until the account is measured again.
|
|
101
|
+
function dropStaleUsage(provider, row) {
|
|
102
|
+
if (!staleQuotaWindows(provider, row.usage?.windows)) return row;
|
|
103
|
+
const fresh = { ...row };
|
|
104
|
+
delete fresh.usage;
|
|
105
|
+
return fresh;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizePool(provider, pool) {
|
|
99
109
|
if (!pool) return pool;
|
|
100
110
|
const policy = providerAccountPolicy(pool);
|
|
101
|
-
|
|
111
|
+
const labelled = normalizeLabels(pool);
|
|
112
|
+
const accounts = labelled.accounts.map((row) => dropStaleUsage(provider, row));
|
|
113
|
+
return { ...labelled, accounts, policy, auto: policy !== 'manual' };
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
export function readProviderAccountPool(provider, dataDir = resolvePluginData()) {
|
|
105
117
|
requireAccountProvider(provider);
|
|
106
118
|
return (
|
|
107
|
-
normalizePool(read(dataDir).providers[provider]) || {
|
|
119
|
+
normalizePool(provider, read(dataDir).providers[provider]) || {
|
|
120
|
+
accounts: [],
|
|
121
|
+
selectedId: null,
|
|
122
|
+
auto: true,
|
|
123
|
+
policy: 'limit',
|
|
124
|
+
}
|
|
108
125
|
);
|
|
109
126
|
}
|
|
110
127
|
|
|
@@ -124,7 +141,7 @@ async function update(provider, mutate) {
|
|
|
124
141
|
},
|
|
125
142
|
{ mode: 0o600, secret: true, fsyncDir: true, backup: true }
|
|
126
143
|
);
|
|
127
|
-
return normalizePool(result.providers[provider]);
|
|
144
|
+
return normalizePool(provider, result.providers[provider]);
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
/**
|
|
@@ -256,21 +273,55 @@ export async function recordProviderAccountUsage(provider, id, snapshot) {
|
|
|
256
273
|
label: String(row.label || '').slice(0, 40),
|
|
257
274
|
usedPct: typeof row.usedPct === 'number' && Number.isFinite(row.usedPct) ? row.usedPct : null,
|
|
258
275
|
resetAt: typeof row.resetAt === 'number' && Number.isFinite(row.resetAt) ? row.resetAt : null,
|
|
276
|
+
...windowScopeFields(row),
|
|
259
277
|
}));
|
|
260
278
|
if (!readProviderAccountPool(provider).accounts.some((row) => row.id === id)) return;
|
|
261
279
|
await update(provider, (pool) => {
|
|
262
280
|
const row = pool.accounts.find((entry) => entry.id === id);
|
|
263
281
|
if (!row) return;
|
|
264
282
|
row.usage = { windows, checkedAt: Date.now() };
|
|
265
|
-
// A fresh observation of available quota releases a prior
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
283
|
+
// A fresh observation of available quota releases a prior refusal. Only
|
|
284
|
+
// the meters the refusal drew on testify (a full Claude/GPT bucket says
|
|
285
|
+
// nothing about a Gemini refusal), and only a MEASURED window: a provider
|
|
286
|
+
// that reports a window without a percentage says nothing about that
|
|
287
|
+
// meter, and requiring a number from it kept accounts refused that every
|
|
288
|
+
// real reading had freed. Each bucket's block is weighed on its own.
|
|
289
|
+
const lifted = (block) => {
|
|
290
|
+
const measured = blockEvidenceWindows(block).filter((window) => window.usedPct !== null);
|
|
291
|
+
return measured.length > 0 && measured.every((window) => window.usedPct < 100);
|
|
292
|
+
};
|
|
293
|
+
setAccountBlocks(
|
|
294
|
+
row,
|
|
295
|
+
accountBlocks(row).filter((block) => !lifted(block))
|
|
296
|
+
);
|
|
271
297
|
});
|
|
272
298
|
}
|
|
273
299
|
|
|
300
|
+
// The model-id tokens a window's gate names, as recorded: lowercased and bounded.
|
|
301
|
+
function modelTokens(value) {
|
|
302
|
+
if (!Array.isArray(value)) return null;
|
|
303
|
+
return value
|
|
304
|
+
.slice(0, 12)
|
|
305
|
+
.map((token) =>
|
|
306
|
+
String(token || '')
|
|
307
|
+
.trim()
|
|
308
|
+
.toLowerCase()
|
|
309
|
+
.slice(0, 80)
|
|
310
|
+
)
|
|
311
|
+
.filter(Boolean);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function windowScopeFields(row) {
|
|
315
|
+
const models = modelTokens(row.models);
|
|
316
|
+
const except = modelTokens(row.except);
|
|
317
|
+
const limit = typeof row.limit === 'string' ? row.limit.trim().slice(0, 80) : '';
|
|
318
|
+
const scope = {};
|
|
319
|
+
if (models) scope.models = models;
|
|
320
|
+
else if (except?.length) scope.except = except;
|
|
321
|
+
if (limit) scope.limit = limit;
|
|
322
|
+
return scope;
|
|
323
|
+
}
|
|
324
|
+
|
|
274
325
|
// A refusal window is an AVOIDANCE hint for the roster, not the retry schedule —
|
|
275
326
|
// the request-level retry honours Retry-After on its own. Generous enough to
|
|
276
327
|
// cover a full five-hour burst window, tight enough that a header asking for
|
|
@@ -283,18 +334,29 @@ const MAX_BLOCK_MS = 6 * 60 * 60_000;
|
|
|
283
334
|
// early can tell us anything.
|
|
284
335
|
// `options.quota` records that the block came from a typed quota refusal or an
|
|
285
336
|
// exhausted usage window, so later sends still count it as quota evidence.
|
|
337
|
+
// `options.limit` is the server's id of the limit the refusal named; the block
|
|
338
|
+
// then covers that limit's windows. `options.reset` marks `until` as the
|
|
339
|
+
// quota reset the server named: it is kept as given (a fresh reading showing
|
|
340
|
+
// room still lifts it early), while advice and inferred delays stay capped.
|
|
341
|
+
// A new refusal of the same model replaces that model's block; every other
|
|
342
|
+
// block still running keeps its own deadline, so refusing one bucket never
|
|
343
|
+
// frees another.
|
|
286
344
|
export async function blockProviderAccount(provider, id, until, model = '', source = 'inferred', options = {}) {
|
|
287
345
|
return update(provider, (pool) => {
|
|
288
346
|
const row = pool.accounts.find((entry) => entry.id === id);
|
|
289
347
|
if (!row) return;
|
|
290
|
-
|
|
348
|
+
const now = Date.now();
|
|
349
|
+
const block = { blockedUntil: options.reset === true ? until : Math.min(until, now + MAX_BLOCK_MS) };
|
|
291
350
|
const scope = String(model || '').trim();
|
|
292
|
-
if (scope)
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
if (options.quota === true)
|
|
297
|
-
|
|
351
|
+
if (scope) block.blockedModel = scope;
|
|
352
|
+
const limit = String(options.limit || '').trim();
|
|
353
|
+
if (limit) block.blockedLimit = limit;
|
|
354
|
+
if (source === 'retry-after') block.blockedSource = source;
|
|
355
|
+
if (options.quota === true) block.blockedQuota = true;
|
|
356
|
+
const running = accountBlocks(row).filter(
|
|
357
|
+
(entry) => entry.blockedUntil > now && String(entry.blockedModel || '') !== scope
|
|
358
|
+
);
|
|
359
|
+
setAccountBlocks(row, [...running, block]);
|
|
298
360
|
});
|
|
299
361
|
}
|
|
300
362
|
|
|
@@ -312,41 +374,136 @@ export async function clearProviderAccountQuotaState(provider, id) {
|
|
|
312
374
|
});
|
|
313
375
|
}
|
|
314
376
|
|
|
377
|
+
const BLOCK_FIELDS = Object.freeze(['blockedUntil', 'blockedModel', 'blockedLimit', 'blockedSource', 'blockedQuota']);
|
|
378
|
+
|
|
315
379
|
function clearAccountBlock(row) {
|
|
316
|
-
delete row
|
|
317
|
-
delete row.
|
|
318
|
-
|
|
319
|
-
|
|
380
|
+
for (const key of BLOCK_FIELDS) delete row[key];
|
|
381
|
+
delete row.blockedBuckets;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const blockOf = (source) =>
|
|
385
|
+
Object.fromEntries(BLOCK_FIELDS.filter((key) => source[key] !== undefined).map((key) => [key, source[key]]));
|
|
386
|
+
|
|
387
|
+
// Every block the account holds, oldest first, each read beside the account's
|
|
388
|
+
// usage. The row's own fields carry the latest; earlier blocks of other
|
|
389
|
+
// buckets still running sit in `blockedBuckets`.
|
|
390
|
+
function accountBlocks(row) {
|
|
391
|
+
const blocks = [
|
|
392
|
+
...(Array.isArray(row?.blockedBuckets) ? row.blockedBuckets : []),
|
|
393
|
+
...(row?.blockedUntil === undefined ? [] : [blockOf(row)]),
|
|
394
|
+
];
|
|
395
|
+
return blocks.map((block) => ({ ...block, usage: row.usage }));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function setAccountBlocks(row, blocks) {
|
|
399
|
+
clearAccountBlock(row);
|
|
400
|
+
if (blocks.length) Object.assign(row, blockOf(blocks.at(-1)));
|
|
401
|
+
if (blocks.length > 1) row.blockedBuckets = blocks.slice(0, -1).map(blockOf);
|
|
402
|
+
return row;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The row holding only the blocks `keep` accepts (none by default), so a check
|
|
406
|
+
// can weigh the usage windows, or only the quota refusals, on their own.
|
|
407
|
+
export function providerAccountWithBlocks(row, keep = () => false) {
|
|
408
|
+
return row && setAccountBlocks({ ...row }, accountBlocks(row).filter(keep));
|
|
320
409
|
}
|
|
321
410
|
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
411
|
+
// Labels every current reading scopes with `models`/`except`. A stored window
|
|
412
|
+
// carrying one of them without a scope predates per-model gating and would
|
|
413
|
+
// read as account-wide, so its whole reading is stale.
|
|
414
|
+
const SCOPED_WINDOW_LABELS = Object.freeze({
|
|
415
|
+
'anthropic-oauth': ['EXTRA'],
|
|
416
|
+
'antigravity-oauth': ['7D Argon', '5H Claude/GPT', '7D Claude/GPT'],
|
|
417
|
+
'cursor-oauth': ['Basic', 'API'],
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
export function staleQuotaWindows(provider, windows) {
|
|
421
|
+
const labels = SCOPED_WINDOW_LABELS[provider] || [];
|
|
422
|
+
return (Array.isArray(windows) ? windows : []).some(
|
|
423
|
+
(window) =>
|
|
424
|
+
labels.includes(String(window?.label || '').trim()) &&
|
|
425
|
+
!Array.isArray(window.models) &&
|
|
426
|
+
!Array.isArray(window.except)
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Which models one quota window gates — the single rule every window and every
|
|
431
|
+
// block follows. The usage reading scopes a window to the server's quota
|
|
432
|
+
// bucket: `models` gates only model ids naming one of those tokens (an empty
|
|
433
|
+
// list gates nothing: a display-only meter such as paid extra usage), `except`
|
|
434
|
+
// gates every model but those. A window without either keeps the older label
|
|
435
|
+
// rule: a single-token weekly label ("7D Fable", "7D Opus") belongs to that
|
|
436
|
+
// family only, and every other window (5H/7D, multi-word surfaces such as
|
|
437
|
+
// "7D OAuth apps") gates every model.
|
|
326
438
|
const FAMILY_QUOTA_WINDOW = /^7D\s+([A-Za-z][A-Za-z0-9]*)$/i;
|
|
327
439
|
|
|
440
|
+
function windowScope(window) {
|
|
441
|
+
if (Array.isArray(window?.models)) return { models: window.models };
|
|
442
|
+
if (Array.isArray(window?.except) && window.except.length) return { except: window.except };
|
|
443
|
+
const family = FAMILY_QUOTA_WINDOW.exec(String(window?.label || '').trim())?.[1];
|
|
444
|
+
return family ? { models: [family.toLowerCase()] } : null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const displayOnly = (window) => Array.isArray(window?.models) && window.models.length === 0;
|
|
448
|
+
|
|
449
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
450
|
+
|
|
451
|
+
function namesModel(model, tokens) {
|
|
452
|
+
const id = String(model).toLowerCase();
|
|
453
|
+
return tokens.some((token) => new RegExp(`(?:^|[-_/.@])${escapeRegExp(token)}(?:[-_/.@]|$)`).test(id));
|
|
454
|
+
}
|
|
455
|
+
|
|
328
456
|
export function quotaWindowAppliesToModel(window, model) {
|
|
329
|
-
const
|
|
330
|
-
if (!
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
457
|
+
const scope = windowScope(window);
|
|
458
|
+
if (!scope) return true;
|
|
459
|
+
if (!model) return false;
|
|
460
|
+
return scope.models ? namesModel(model, scope.models) : !namesModel(model, scope.except);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// The meters a block stands for: the windows of the limit the refusal named,
|
|
464
|
+
// else the scoped windows that gate the model that earned it.
|
|
465
|
+
function blockWindows(row) {
|
|
466
|
+
const windows = row?.usage?.windows || [];
|
|
467
|
+
const limit = String(row?.blockedLimit || '');
|
|
468
|
+
const named = limit ? windows.filter((window) => window.limit === limit) : [];
|
|
469
|
+
if (named.length) return named;
|
|
470
|
+
const earned = String(row?.blockedModel || '').trim();
|
|
471
|
+
return earned ? windows.filter((window) => windowScope(window) && quotaWindowAppliesToModel(window, earned)) : [];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// The windows a fresh reading must show below 100% to lift a block: the
|
|
475
|
+
// block's own meters plus every meter, scoped or shared, the model that earned
|
|
476
|
+
// it (or, for an account-wide block, any model) draws on.
|
|
477
|
+
function blockEvidenceWindows(row) {
|
|
478
|
+
const earned = String(row?.blockedModel || '').trim();
|
|
479
|
+
const gating = (row?.usage?.windows || []).filter((window) =>
|
|
480
|
+
earned ? quotaWindowAppliesToModel(window, earned) : !displayOnly(window)
|
|
481
|
+
);
|
|
482
|
+
return [...new Set([...blockWindows(row), ...gating])];
|
|
335
483
|
}
|
|
336
484
|
|
|
337
485
|
// A refusal earned by one model must not idle a model that draws on a
|
|
338
|
-
// different meter — the same rule the usage windows
|
|
339
|
-
//
|
|
340
|
-
// scoped meter
|
|
341
|
-
// because there is no narrower category to attribute it to.
|
|
486
|
+
// different meter — the same rule the usage windows follow. Two models share a
|
|
487
|
+
// block when one of the block's windows gates both; when the account exposes
|
|
488
|
+
// no scoped meter for the model (a plain 5H/7D subscription) the block stays
|
|
489
|
+
// account-wide, because there is no narrower category to attribute it to.
|
|
342
490
|
function blockAppliesToModel(row, model) {
|
|
343
|
-
const
|
|
344
|
-
if (!
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
|
|
491
|
+
const earned = String(row?.blockedModel || '').trim();
|
|
492
|
+
if (!earned || !model || earned === String(model)) return true;
|
|
493
|
+
const windows = blockWindows(row);
|
|
494
|
+
if (!windows.length) return true;
|
|
495
|
+
return windows.some((window) => quotaWindowAppliesToModel(window, model));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// When the blocks holding `model` lift (every block when no model is named):
|
|
499
|
+
// the latest of their deadlines, or null when none holds it. Past deadlines
|
|
500
|
+
// are returned as they are, so a caller compares the result with its clock.
|
|
501
|
+
export function providerAccountBlockedUntil(row, model = '') {
|
|
502
|
+
const ends = accountBlocks(row)
|
|
503
|
+
.filter((block) => blockAppliesToModel(block, model))
|
|
504
|
+
.map((block) => Number(block.blockedUntil))
|
|
505
|
+
.filter(Number.isFinite);
|
|
506
|
+
return ends.length ? Math.max(...ends) : null;
|
|
350
507
|
}
|
|
351
508
|
|
|
352
509
|
// Earliest moment any of the given exhausted accounts can serve `model` again,
|
|
@@ -360,8 +517,9 @@ export function providerAccountsResetAt(rows, now = Date.now(), model = '', opti
|
|
|
360
517
|
if (!providerAccountExhausted(row, now, model, options)) continue;
|
|
361
518
|
const times = [];
|
|
362
519
|
let unknown = false;
|
|
363
|
-
|
|
364
|
-
if (
|
|
520
|
+
for (const block of accountBlocks(row)) {
|
|
521
|
+
if (!(block.blockedUntil > now) || !blockAppliesToModel(block, model)) continue;
|
|
522
|
+
if (block.blockedSource === 'retry-after') times.push(block.blockedUntil);
|
|
365
523
|
else unknown = true;
|
|
366
524
|
}
|
|
367
525
|
if (options.useCredits !== true) {
|
|
@@ -381,39 +539,47 @@ function windowFull(row, window, now) {
|
|
|
381
539
|
}
|
|
382
540
|
|
|
383
541
|
export function providerAccountExhausted(row, now = Date.now(), model = '', options = {}) {
|
|
384
|
-
if (row
|
|
542
|
+
if (accountBlocks(row).some((block) => block.blockedUntil > now && blockAppliesToModel(block, model))) return true;
|
|
385
543
|
if (options.useCredits === true) return false;
|
|
386
544
|
return (row?.usage?.windows || []).some(
|
|
387
545
|
(window) => quotaWindowAppliesToModel(window, model) && windowFull(row, window, now)
|
|
388
546
|
);
|
|
389
547
|
}
|
|
390
548
|
|
|
549
|
+
// The display name of a scoped limit: the family of a legacy "7D Fable" label,
|
|
550
|
+
// else the label without its window prefix ("5H Claude/GPT" → "Claude/GPT").
|
|
391
551
|
function quotaWindowFamily(window) {
|
|
392
|
-
|
|
552
|
+
const label = String(window?.label || '').trim();
|
|
553
|
+
return FAMILY_QUOTA_WINDOW.exec(label)?.[1] || label.replace(/^(?:5H|7D)\s+/i, '') || label;
|
|
393
554
|
}
|
|
394
555
|
|
|
395
556
|
// Which limit currently holds the account, independent of any one model:
|
|
396
557
|
// `{ scope: 'account' }` when every request is refused (an unscoped window is
|
|
397
558
|
// full, or a block with no narrower meter to attribute it to), `{ scope:
|
|
398
|
-
// 'model', families }` when only
|
|
399
|
-
// attributed to one are spent, so other models still run — else null.
|
|
559
|
+
// 'model', families }` when only scoped meters ("7D Fable", "5H Claude/GPT")
|
|
560
|
+
// or a block attributed to one are spent, so other models still run — else null.
|
|
400
561
|
export function providerAccountLimit(row, now = Date.now(), options = {}) {
|
|
401
562
|
if (!row) return null;
|
|
402
563
|
const windows = row.usage?.windows || [];
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
564
|
+
// A block counts as scoped only when scoped meters cover it; any other
|
|
565
|
+
// refusal is treated as the account's.
|
|
566
|
+
const covering = accountBlocks(row)
|
|
567
|
+
.filter((block) => block.blockedUntil > now)
|
|
568
|
+
.map(blockWindows);
|
|
569
|
+
if (covering.some((meters) => !meters.length || meters.some((window) => !windowScope(window)))) {
|
|
570
|
+
return { scope: 'account' };
|
|
571
|
+
}
|
|
410
572
|
const credits = options.useCredits === true;
|
|
411
|
-
if (!credits && windows.some((window) => !
|
|
573
|
+
if (!credits && windows.some((window) => !windowScope(window) && windowFull(row, window, now))) {
|
|
412
574
|
return { scope: 'account' };
|
|
413
575
|
}
|
|
414
|
-
const families = new Set(
|
|
576
|
+
const families = new Set(covering.flat().map(quotaWindowFamily));
|
|
415
577
|
if (!credits) {
|
|
416
|
-
for (const window of
|
|
578
|
+
for (const window of windows) {
|
|
579
|
+
if (windowScope(window) && !displayOnly(window) && windowFull(row, window, now)) {
|
|
580
|
+
families.add(quotaWindowFamily(window));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
417
583
|
}
|
|
418
584
|
return families.size ? { scope: 'model', families: [...families] } : null;
|
|
419
585
|
}
|
|
@@ -26,16 +26,27 @@ function providerSecretEnv(provider) {
|
|
|
26
26
|
.toUpperCase()}_APIKEY`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// The environment variable that supplies a provider's key, in the resolver's
|
|
30
|
+
// precedence (standard name, MIXDOG_AGENT_<ID>_APIKEY, then aliases); null when none is set.
|
|
31
|
+
function environmentKeyName(id) {
|
|
31
32
|
const standardEnv = AGENT_PROVIDER_ENV[id];
|
|
32
|
-
if (standardEnv && process.env[standardEnv]) return
|
|
33
|
+
if (standardEnv && process.env[standardEnv]) return standardEnv;
|
|
33
34
|
const mixdogEnv = providerSecretEnv(id);
|
|
34
|
-
if (process.env[mixdogEnv]) return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
if (process.env[mixdogEnv]) return mixdogEnv;
|
|
36
|
+
return (AGENT_PROVIDER_ENV_ALIASES[id] || []).find((alias) => process.env[alias]) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function environmentApiKey(provider) {
|
|
40
|
+
const name = environmentKeyName(String(provider || '').trim());
|
|
41
|
+
return name ? process.env[name] : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Name of the environment variable getAgentApiKey would return for `provider`
|
|
45
|
+
* (it wins over any stored key), or null when the effective key is not an
|
|
46
|
+
* environment one. Provenance for the setup views; never the value. */
|
|
47
|
+
export function agentApiKeyEnvName(provider) {
|
|
48
|
+
if (modelProxyClientMode()) return null;
|
|
49
|
+
return environmentKeyName(String(provider || '').trim());
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
/** Presence-only credential probe. On Windows this is a cheap DPAPI-file
|
|
@@ -28,6 +28,48 @@ export function routineTriggerHeader({ kind, name, project = '', now = new Date(
|
|
|
28
28
|
].join(' ');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** The whole reply of a heartbeat turn that has nothing to tell the user. */
|
|
32
|
+
export const HEARTBEAT_QUIET = 'HEARTBEAT_QUIET';
|
|
33
|
+
/** The routine name a heartbeat turn runs under and its hidden session's source. */
|
|
34
|
+
export const HEARTBEAT_SOURCE = 'heartbeat';
|
|
35
|
+
/** A heartbeat turn's bounds: its tool calls; when it is told to reply with no
|
|
36
|
+
* more tools (session/loop/turn-limits.mjs); when it is cut off without a
|
|
37
|
+
* reply (session-runtime/routine-home-turn.mjs). */
|
|
38
|
+
export const HEARTBEAT_TURN_LIMITS = Object.freeze({ maxToolCalls: 8, wrapUpMs: 60_000, wallClockMs: 90_000 });
|
|
39
|
+
|
|
40
|
+
// Mixy's hidden heartbeat sessions (mixy-heartbeat.mjs wakeMixy), marked in
|
|
41
|
+
// this process from creation until their close lands. Only the mark counts:
|
|
42
|
+
// stored fields (source, owner) can be supplied by any session create.
|
|
43
|
+
const heartbeatSessions = new Set();
|
|
44
|
+
|
|
45
|
+
/** Mark `sessionId` as a heartbeat session; `unmark` ends it. */
|
|
46
|
+
export function markHeartbeatSession(sessionId, unmark = false) {
|
|
47
|
+
const id = String(sessionId || '');
|
|
48
|
+
if (unmark) heartbeatSessions.delete(id);
|
|
49
|
+
else if (id) heartbeatSessions.add(id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether `sessionId` is a marked heartbeat session. */
|
|
53
|
+
export function isHeartbeatSession(sessionId) {
|
|
54
|
+
return heartbeatSessions.has(String(sessionId || ''));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The first line of a heartbeat turn (session-runtime/mixy-heartbeat.mjs):
|
|
58
|
+
* a system check-in, never the user, and it grants no approval. */
|
|
59
|
+
export function heartbeatTriggerHeader({ now = new Date() } = {}) {
|
|
60
|
+
const at = localTimestampParts(now);
|
|
61
|
+
return [
|
|
62
|
+
`${HEADER_PREFIX} Heartbeat: a system check-in from Mixdog, not a message from the user; it grants no approval.`,
|
|
63
|
+
...(at ? [`Local time: ${at.date} ${at.time.slice(0, 5)} ${at.timeZone} (${at.offset}).`] : []),
|
|
64
|
+
'Nobody is watching this turn and nothing asks for approval: act within your permissions.',
|
|
65
|
+
'Payments and permanent deletes are refused here.',
|
|
66
|
+
'Do not start agents, task sessions, session sends, Goals or background commands here: this session closes when the turn ends, so their results would reach nobody; propose them instead.',
|
|
67
|
+
'Handle the items below as your heartbeat instructions say, finishing in this turn:',
|
|
68
|
+
'only its final reply is posted in your chat with the user, as your message.',
|
|
69
|
+
`If nothing is worth telling the user, reply with exactly ${HEARTBEAT_QUIET} and nothing else.`,
|
|
70
|
+
].join(' ');
|
|
71
|
+
}
|
|
72
|
+
|
|
31
73
|
/** `text` without the header line a routine turn opens with. */
|
|
32
74
|
export function stripRoutineTriggerHeader(text) {
|
|
33
75
|
const value = String(text ?? '');
|
|
@@ -195,6 +195,8 @@ const TRANSCRIPT_HIDDEN_CONTROL_TOOL_NAMES = new Set([
|
|
|
195
195
|
'update_goal',
|
|
196
196
|
'load_tool',
|
|
197
197
|
'tool_search',
|
|
198
|
+
// A bot's reaction shows as its emoji under the user's message, not as a card.
|
|
199
|
+
'react',
|
|
198
200
|
]);
|
|
199
201
|
// Skill loads are visible cards EXCEPT for built-in skills: those ride a
|
|
200
202
|
// built-in feature whose own card (Browser Use, Document work, ...) follows
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// These pure helpers keep live rows, persisted messages and restored rows on
|
|
3
3
|
// the same field contract without rewriting older display-only transcripts.
|
|
4
4
|
import { displayModelName } from './model-display.mjs';
|
|
5
|
+
import { botChainOf } from './bot-hops.mjs';
|
|
5
6
|
import { routineTriggerField } from './routine-trigger.mjs';
|
|
6
7
|
|
|
7
8
|
export function transcriptRouteMetadataFields(value) {
|
|
@@ -90,6 +91,11 @@ export function persistedUserTranscriptMetadata(value) {
|
|
|
90
91
|
...transcriptSourceFields(value),
|
|
91
92
|
// Bot-to-bot chain position of a bot's message (session-runtime/bot-hops.mjs).
|
|
92
93
|
...(Number.isInteger(value.hop) && value.hop > 0 ? { hop: value.hop } : {}),
|
|
94
|
+
// The bots that handed this message on, sender last (bot-hops.mjs botChainOf).
|
|
95
|
+
...botChainField(value.botChain),
|
|
96
|
+
// A group-room member turn: the newest user row of the room log its prompt
|
|
97
|
+
// shows (message-reactions.mjs counts a reaction only on that row).
|
|
98
|
+
...(typeof value.roomRow === 'string' && value.roomRow ? { roomRow: value.roomRow } : {}),
|
|
93
99
|
// The turn's change-record key (the outer prompt id), so a restored user
|
|
94
100
|
// row still addresses its turn's Undo/Reapply.
|
|
95
101
|
...(typeof value.checkpointId === 'string' && value.checkpointId ? { checkpointId: value.checkpointId } : {}),
|
|
@@ -104,6 +110,11 @@ export function persistedUserTranscriptMetadata(value) {
|
|
|
104
110
|
};
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
function botChainField(value) {
|
|
114
|
+
const bots = botChainOf(value);
|
|
115
|
+
return bots.length ? { botChain: bots } : {};
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
function submissionIdsField(value) {
|
|
108
119
|
const ids = Array.isArray(value) ? value.filter((id) => typeof id === 'string' && id) : [];
|
|
109
120
|
return ids.length ? { submissionIds: ids } : {};
|
|
@@ -125,6 +136,33 @@ export function transcriptAttachmentFields(value) {
|
|
|
125
136
|
return attachmentsField(value?.attachments);
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
const REACTOR_ID = /^[A-Za-z0-9._-]{1,80}$/;
|
|
140
|
+
const REACTION_EMOJI_MAX_LENGTH = 16;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The bots' reactions on a user message (`meta.transcript.reactions`):
|
|
144
|
+
* `[{ agentId, emoji, at }]`, one per bot, in the order they were left.
|
|
145
|
+
* Malformed entries and a bot's repeats are dropped.
|
|
146
|
+
*/
|
|
147
|
+
export function normalizeMessageReactions(value) {
|
|
148
|
+
if (!Array.isArray(value)) return [];
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const raw of value) {
|
|
151
|
+
const agentId = typeof raw?.agentId === 'string' ? raw.agentId : '';
|
|
152
|
+
const emoji = typeof raw?.emoji === 'string' ? raw.emoji : '';
|
|
153
|
+
if (!REACTOR_ID.test(agentId) || !emoji || emoji.length > REACTION_EMOJI_MAX_LENGTH) continue;
|
|
154
|
+
if (out.some((entry) => entry.agentId === agentId)) continue;
|
|
155
|
+
out.push({ agentId, emoji, ...(Number.isFinite(raw.at) ? { at: raw.at } : {}) });
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The `reactions` field of a transcript row (absent when there are none). */
|
|
161
|
+
export function transcriptReactionsField(value) {
|
|
162
|
+
const reactions = normalizeMessageReactions(value?.reactions);
|
|
163
|
+
return reactions.length ? { reactions } : {};
|
|
164
|
+
}
|
|
165
|
+
|
|
128
166
|
export function persistedAssistantTranscriptMetadata(value, fallbackAt = Date.now()) {
|
|
129
167
|
if (!value || typeof value !== 'object') return null;
|
|
130
168
|
const candidateAt = Number(value.assistantAt);
|
|
@@ -438,13 +438,41 @@ const ensuredDataDirs = new Set();
|
|
|
438
438
|
* data dir per process.
|
|
439
439
|
*/
|
|
440
440
|
export function ensureUserDataInitialized(dataDir) {
|
|
441
|
-
if (!dataDir) return;
|
|
441
|
+
if (!dataDir || skipBackups()) return;
|
|
442
442
|
const key = normalizeDataDir(dataDir);
|
|
443
443
|
if (ensuredDataDirs.has(key)) return;
|
|
444
|
-
|
|
444
|
+
// The marker alone is no proof: a first snapshot whose manifest failed left
|
|
445
|
+
// it behind. Only a published backup of this incarnation counts, so a failed
|
|
446
|
+
// or interrupted one is taken again by the next read or restart.
|
|
447
|
+
if (!hasPublishedBackup(dataDir)) {
|
|
448
|
+
backupUserData(dataDir, 'initial-snapshot');
|
|
449
|
+
if (!hasPublishedBackup(dataDir)) return;
|
|
450
|
+
}
|
|
445
451
|
ensuredDataDirs.add(key);
|
|
446
452
|
}
|
|
447
453
|
|
|
454
|
+
/** Whether this data dir's scope holds a published backup of its current
|
|
455
|
+
* incarnation (marker instanceId). A transient manifest read failure throws. */
|
|
456
|
+
function hasPublishedBackup(dataDir) {
|
|
457
|
+
const instanceId = readInitInstanceId(dataDir);
|
|
458
|
+
if (!instanceId) return false;
|
|
459
|
+
const key = normalizeDataDir(dataDir);
|
|
460
|
+
const scopeDir = scopeDirFor(dataDir);
|
|
461
|
+
return listBackupDirsNewestFirst(scopeDir).some((name) => {
|
|
462
|
+
try {
|
|
463
|
+
const manifest = JSON.parse(readFileSync(manifestPath(scopeDir, name), 'utf8'));
|
|
464
|
+
return (
|
|
465
|
+
typeof manifest?.dataDir === 'string' &&
|
|
466
|
+
normalizeDataDir(manifest.dataDir) === key &&
|
|
467
|
+
manifest.instanceId === instanceId
|
|
468
|
+
);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (isAbsentBackupError(error)) return false;
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
448
476
|
function skipBackups() {
|
|
449
477
|
return process.env.MIXDOG_SKIP_USER_DATA_BACKUP === '1' || process.env.MIXDOG_SKIP_USER_DATA_BACKUP === 'true';
|
|
450
478
|
}
|
|
@@ -15,9 +15,11 @@ const overlaps = (a, b) => isWithin(a, b) || isWithin(b, a);
|
|
|
15
15
|
/**
|
|
16
16
|
* The admission policy's external exception: a linked checkout Mixdog did not
|
|
17
17
|
* create (not a project's main folder, not a registered managed worktree) is
|
|
18
|
-
*
|
|
19
|
-
* admitted whatever is held
|
|
20
|
-
* management action that touches it.
|
|
18
|
+
* not lock-blocked by what is held elsewhere. Management takes no lease on it
|
|
19
|
+
* and its readers are admitted whatever is held outside it; its running
|
|
20
|
+
* readers still refuse a management action that touches it. A management lease
|
|
21
|
+
* published inside the checkout itself (another profile, or a registry
|
|
22
|
+
* recovered empty) still refuses its readers.
|
|
21
23
|
*/
|
|
22
24
|
export function isExternalCheckout(folder) {
|
|
23
25
|
const layout = findGitLayout(folder, { strict: true });
|
|
@@ -291,7 +293,8 @@ function release(commonDir, token) {
|
|
|
291
293
|
|
|
292
294
|
/**
|
|
293
295
|
* Readers share a checkout freely. Null means a management writer currently
|
|
294
|
-
* owns it; a reader in an external checkout is
|
|
296
|
+
* owns it; a reader in an external checkout is admitted whatever is held
|
|
297
|
+
* outside that checkout, but refused by a management lease inside it. Throws
|
|
295
298
|
* `workspace-admission-unreadable` when the ledger cannot be read, was
|
|
296
299
|
* written by a newer build, or the grant could not be made durable in both
|
|
297
300
|
* copies: no work may run unrecorded.
|
|
@@ -304,8 +307,12 @@ export function tryAdmitWorkspaceActivity(sessionId, cwd, { pid = process.pid, k
|
|
|
304
307
|
const path = canonicalPath(cwd);
|
|
305
308
|
const token = randomUUID();
|
|
306
309
|
const lease = { kind, sessionId, pid, token, path };
|
|
310
|
+
// External is this profile's registry view: management another profile (or
|
|
311
|
+
// a registry recovered empty) published inside this very checkout still holds.
|
|
312
|
+
const checkout = canonicalPath(layout.topLevel);
|
|
313
|
+
const held = (other) => (external ? isWithin(checkout, other.path) : overlaps(other.path, path));
|
|
307
314
|
const admitted = mutate(layout.commonDir, (leases) => {
|
|
308
|
-
if (
|
|
315
|
+
if (leases.some((other) => other.kind === 'management' && held(other))) return false;
|
|
309
316
|
leases.push(lease);
|
|
310
317
|
return true;
|
|
311
318
|
});
|