maka-agent 0.2.0-dev.22.20260905 → 0.2.0-dev.23.20260906
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/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/bot-events.js +18 -63
- package/node_modules/@maka/core/dist/model-call-attempt.js +0 -82
- package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -186
- package/node_modules/@maka/core/dist/record-schema.js +13 -0
- package/node_modules/@maka/core/dist/redaction.js +41 -58
- package/node_modules/@maka/core/dist/session-trace.js +5 -1
- package/node_modules/@maka/core/dist/usage-ledger-merge.js +7 -9
- package/node_modules/@maka/core/dist/workhub-creation-intent.js +34 -20
- package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +2 -1
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +51 -24
- package/node_modules/@maka/runtime/dist/bots/base-adapter.js +17 -0
- package/node_modules/@maka/runtime/dist/bots/bot-test.js +38 -22
- package/node_modules/@maka/runtime/dist/bots/dingtalk-bridge.js +7 -5
- package/node_modules/@maka/runtime/dist/bots/discord-bridge.js +4 -4
- package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/qq-bridge.js +6 -4
- package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +3 -4
- package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +17 -19
- package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +5 -6
- package/node_modules/@maka/runtime/dist/bots/ws-bridge-base.js +2 -3
- package/node_modules/@maka/runtime/dist/session-trace-projection.js +3 -30
- package/node_modules/@maka/runtime-host/dist/candidate-entry.js +4 -1
- package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +13 -0
- package/node_modules/@maka/runtime-host/dist/client/registered-host-termination.js +0 -59
- package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +28 -16
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +4 -0
- package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +8 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +13 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +28 -1
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +33 -0
- package/node_modules/@maka/runtime-host/dist/server/access-authority.js +26 -1
- package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +5 -1
- package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -11
- package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +7 -4
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +70 -6
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +6 -3
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +12 -6
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +9 -2
- package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +7 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +21 -0
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +151 -5
- package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +4 -11
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -15
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +6 -1
- package/node_modules/@maka/storage/dist/agent-run-store.js +44 -0
- package/node_modules/@maka/storage/dist/conversation-operational-state.js +4 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +2 -0
- package/node_modules/@maka/storage/dist/model-call-ledger.js +199 -32
- package/node_modules/@maka/storage/dist/model-call-usage-sql.js +136 -0
- package/node_modules/@maka/storage/dist/operational-state-store-public.js +1 -1
- package/node_modules/@maka/storage/dist/operational-state-store.js +5 -3
- package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +11 -1
- package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +200 -20
- package/node_modules/@maka/storage/dist/usage-stores.js +6 -2
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { RuntimeHostProtocolError } from '../protocol/errors.js';
|
|
20
20
|
import { createHash } from 'node:crypto';
|
|
21
|
-
import { authorizeConnectionModel } from '@maka/core/llm-connections';
|
|
21
|
+
import { authorizeConnectionModel, connectionEnabledModelIds } from '@maka/core/llm-connections';
|
|
22
22
|
import { isModelExplicitlyUnsupportedForChat } from '@maka/core/model-catalog';
|
|
23
23
|
import { thinkingVariantsForConnection } from '@maka/core/model-thinking';
|
|
24
24
|
import { executionBoundaryDisplayMode, } from '@maka/core/sandbox-boundary';
|
|
@@ -37,6 +37,84 @@ export class SessionOperationFailure extends Error {
|
|
|
37
37
|
this.name = 'SessionOperationFailure';
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The import path found no ready connection+model to attach the task to. A
|
|
42
|
+
* distinct type (not just a message) so `#importSession` can map it to the
|
|
43
|
+
* stable `model_unavailable` wire code without inspecting the message — the
|
|
44
|
+
* generic `operation_unavailable` code it carries is also used for an
|
|
45
|
+
* unavailable source, which is a different failure.
|
|
46
|
+
*/
|
|
47
|
+
export class NoUsableImportModelError extends SessionOperationFailure {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super('operation_unavailable', message);
|
|
50
|
+
this.name = 'NoUsableImportModelError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Connection+model candidates for an imported task, most-preferred first: the
|
|
55
|
+
* configured default (kept at its exact precedence), then one ready model per
|
|
56
|
+
* enabled connection in catalog order. Model-level readiness that is a pure
|
|
57
|
+
* catalog fact — enabled, not quarantined, chat-capable — is applied here so an
|
|
58
|
+
* unusable connection costs one `#resolveModel` attempt, not one per enabled
|
|
59
|
+
* model (a connection may enable hundreds). Connection-level readiness
|
|
60
|
+
* (credential, retired provider, identity) stays in `#resolveModel`, which
|
|
61
|
+
* remains the sole arbiter of those.
|
|
62
|
+
*/
|
|
63
|
+
function importModelCandidates(snapshot) {
|
|
64
|
+
const byId = new Map(snapshot.connections.map((connection) => [connection.connectionId, connection]));
|
|
65
|
+
const candidates = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const push = (connection, modelId, isDefault) => {
|
|
68
|
+
const key = `${connection.connectionId} ${modelId}`;
|
|
69
|
+
if (seen.has(key))
|
|
70
|
+
return;
|
|
71
|
+
seen.add(key);
|
|
72
|
+
candidates.push({
|
|
73
|
+
connectionId: connection.connectionId,
|
|
74
|
+
connectionSlug: connection.slug,
|
|
75
|
+
modelId,
|
|
76
|
+
isDefault,
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
// The connection's first enabled model that is a valid chat target, by pure
|
|
80
|
+
// catalog facts alone (no credential or provider-liveness read). Emitting only
|
|
81
|
+
// this one — rather than every enabled model — bounds the enumeration to one
|
|
82
|
+
// candidate per connection, so a connection-level failure (missing credential,
|
|
83
|
+
// retired provider) costs a single `#resolveModel` round trip.
|
|
84
|
+
const firstReadyModel = (connection) => {
|
|
85
|
+
for (const modelId of connectionEnabledModelIds(connection)) {
|
|
86
|
+
// Mirror `#resolveModel`'s pure-catalog gates, the wire byte cap included:
|
|
87
|
+
// a model id can be within the catalog's code-unit limit yet exceed the
|
|
88
|
+
// byte cap (e.g. emoji), and taking it as the connection's sole candidate
|
|
89
|
+
// would let `#resolveModel` reject it and mask the connection's shorter,
|
|
90
|
+
// usable models. Skip it here so the next enabled model is considered.
|
|
91
|
+
if (Buffer.byteLength(modelId, 'utf8') > SESSION_CATALOG_MODEL_MAX_BYTES)
|
|
92
|
+
continue;
|
|
93
|
+
const model = authorizeConnectionModel(connection, modelId);
|
|
94
|
+
if (model && !isModelExplicitlyUnsupportedForChat(model))
|
|
95
|
+
return modelId;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
99
|
+
// Default first, so a configured-and-ready default keeps today's behavior.
|
|
100
|
+
// `retainedDefaultTarget` + `isValidTarget` guarantee a persisted default is
|
|
101
|
+
// enabled and present in `enabledModelIds`, so its exact model is taken at its
|
|
102
|
+
// precedence rather than re-picked from the connection.
|
|
103
|
+
const preferred = snapshot.defaultTarget;
|
|
104
|
+
if (preferred) {
|
|
105
|
+
const connection = byId.get(preferred.connectionId);
|
|
106
|
+
if (connection?.enabled)
|
|
107
|
+
push(connection, preferred.modelId, true);
|
|
108
|
+
}
|
|
109
|
+
for (const connection of snapshot.connections) {
|
|
110
|
+
if (!connection.enabled)
|
|
111
|
+
continue;
|
|
112
|
+
const modelId = firstReadyModel(connection);
|
|
113
|
+
if (modelId !== undefined)
|
|
114
|
+
push(connection, modelId, false);
|
|
115
|
+
}
|
|
116
|
+
return candidates;
|
|
117
|
+
}
|
|
40
118
|
/** Host-owned Session catalog, creation, and configuration authority. */
|
|
41
119
|
export class HostSessionCatalogCoordinator {
|
|
42
120
|
handlers = {
|
|
@@ -69,11 +147,29 @@ export class HostSessionCatalogCoordinator {
|
|
|
69
147
|
this.#requestDrain = options.requestDrain;
|
|
70
148
|
this.#sessionAccessAuthority = options.sessionAccessAuthority;
|
|
71
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Target for a task imported from another agent's conversation. Import is an
|
|
152
|
+
* explicit, one-off user action on a specific conversation, so it prefers the
|
|
153
|
+
* configured default but falls back to any ready connection+model — see
|
|
154
|
+
* `#resolveImportModel`.
|
|
155
|
+
*/
|
|
72
156
|
async resolveExternalSessionImportTarget() {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
157
|
+
return this.#composeCreateTarget(this.#resolveImportModel());
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Target for the autonomous WorkHub coordination create path (its
|
|
161
|
+
* `resolveCreateTarget`). Unlike import there is no user in the loop to pick a
|
|
162
|
+
* model, so this fails closed when no default is configured rather than binding
|
|
163
|
+
* a connection the user never chose. Import's fallback deliberately does not
|
|
164
|
+
* reach here; keeping the two resolvers apart is what confines the guess to an
|
|
165
|
+
* explicit user
|
|
166
|
+
* action.
|
|
167
|
+
*/
|
|
168
|
+
async resolveDefaultCreateTarget() {
|
|
169
|
+
return this.#composeCreateTarget(this.#resolveModel({ kind: 'default' }, undefined));
|
|
170
|
+
}
|
|
171
|
+
async #composeCreateTarget(modelResolution) {
|
|
172
|
+
const [model, policy] = await Promise.all([modelResolution, this.#readRuntimePolicy()]);
|
|
77
173
|
return {
|
|
78
174
|
llmConnectionId: model.connectionId,
|
|
79
175
|
llmConnectionSlug: model.connectionSlug,
|
|
@@ -511,6 +607,56 @@ export class HostSessionCatalogCoordinator {
|
|
|
511
607
|
throw error;
|
|
512
608
|
}
|
|
513
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* Model for an imported task. Prefers the configured default but falls back to
|
|
612
|
+
* any ready connection+model, because a default is only auto-set during
|
|
613
|
+
* onboarding bootstrap (`setDefaultIfMissing`) and a self-configured profile
|
|
614
|
+
* legitimately has `defaultTarget: null` while holding perfectly usable
|
|
615
|
+
* connections. Without the fallback, every import fails before the source is
|
|
616
|
+
* even read. Unlike interactive session creation, import has no model picker,
|
|
617
|
+
* so this is the only place that can choose one.
|
|
618
|
+
*/
|
|
619
|
+
async #resolveImportModel() {
|
|
620
|
+
let snapshot;
|
|
621
|
+
try {
|
|
622
|
+
snapshot = await this.#runtimePolicy.connectionCatalog.getSnapshot();
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
throw new SessionOperationFailure('persistence_failed', 'Connection catalog is unavailable');
|
|
626
|
+
}
|
|
627
|
+
for (const candidate of importModelCandidates(snapshot)) {
|
|
628
|
+
try {
|
|
629
|
+
return await this.#resolveModel({
|
|
630
|
+
kind: 'explicit',
|
|
631
|
+
connectionId: candidate.connectionId,
|
|
632
|
+
connectionSlug: candidate.connectionSlug,
|
|
633
|
+
model: candidate.modelId,
|
|
634
|
+
}, undefined);
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
if (!(error instanceof SessionOperationFailure))
|
|
638
|
+
throw error;
|
|
639
|
+
// The configured default is never substituted away from: if it is set
|
|
640
|
+
// but unusable (e.g. its credential was revoked), surface its failure
|
|
641
|
+
// exactly as an explicit default target does today, rather than silently
|
|
642
|
+
// attaching the task to a connection the user did not choose. The
|
|
643
|
+
// fallback below runs only when no default is set — the null-default
|
|
644
|
+
// state this fix is for, where there is nothing to substitute for.
|
|
645
|
+
if (candidate.isDefault)
|
|
646
|
+
throw error;
|
|
647
|
+
// Otherwise, only a genuinely unusable candidate is skippable:
|
|
648
|
+
// `invalid_request` (disabled / retired / not-enabled / non-chat) and
|
|
649
|
+
// `operation_unavailable` (no credential). Any other code — notably
|
|
650
|
+
// `operation_conflict`, thrown when the connection was deleted or
|
|
651
|
+
// renamed after the snapshot — is a real fault the caller must see, not
|
|
652
|
+
// a reason to silently fall through to a lower-priority connection.
|
|
653
|
+
if (error.code !== 'invalid_request' && error.code !== 'operation_unavailable') {
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
throw new NoUsableImportModelError('No usable Session model connection is available for import');
|
|
659
|
+
}
|
|
514
660
|
async #resolveModel(target, thinkingLevel) {
|
|
515
661
|
const selected = await this.#selectModelTarget(target);
|
|
516
662
|
const readiness = await this.#runtimePolicy.operations.resolveExecutionConnection({
|
|
@@ -23,7 +23,7 @@ import { BUILTIN_PRICING } from '@maka/runtime/telemetry';
|
|
|
23
23
|
import { authenticateInteractiveUsageStoresWriter, classifyInteractiveUsageStoresFailure, } from '@maka/storage/usage-stores';
|
|
24
24
|
import { isSessionNotFoundError } from '@maka/storage/execution-stores';
|
|
25
25
|
import { encodePricingQueryResult, encodeUsageQueryResult, PRICING_PAGE_MAX_BYTES, PRICING_PAGE_MAX_ITEMS, USAGE_PAGE_MAX_BYTES, USAGE_PAGE_MAX_ITEMS, USAGE_PROJECTION_TEXT_MAX_BYTES, } from '../protocol/index.js';
|
|
26
|
-
import {
|
|
26
|
+
import { readCanonicalUsageBuckets, readCanonicalUsageLogs, readCanonicalUsageSummary, } from './canonical-usage-reader.js';
|
|
27
27
|
/** Root-scoped projection over the authentic lease-bound usage stores. */
|
|
28
28
|
export class HostUsagePricingCoordinator {
|
|
29
29
|
handlers = {
|
|
@@ -75,18 +75,11 @@ export class HostUsagePricingCoordinator {
|
|
|
75
75
|
}));
|
|
76
76
|
return titles;
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Reads the canonical ledger for the window a query addresses (#1679). The
|
|
80
|
-
* range is resolved once here so both sources answer the same window.
|
|
81
|
-
*/
|
|
82
|
-
async #canonicalUsage(query, now, repair = true) {
|
|
83
|
-
return readCanonicalUsage(this.#stores, query, now, repair);
|
|
84
|
-
}
|
|
85
78
|
async #queryUsage(input) {
|
|
86
79
|
try {
|
|
87
80
|
const now = Date.now();
|
|
88
81
|
if (input.kind === 'summary') {
|
|
89
|
-
const merged = mergeUsageSummary(await this.#stores.telemetry.summary(input.query), await this.#
|
|
82
|
+
const merged = mergeUsageSummary(await this.#stores.telemetry.summary(input.query), await readCanonicalUsageSummary(this.#stores, input.query, now));
|
|
90
83
|
// Tool executions are in their own ledger, not the model-call one, so
|
|
91
84
|
// their totals ride beside the merged summary rather than inside it —
|
|
92
85
|
// the same owner split the tool buckets path already follows. A
|
|
@@ -117,7 +110,7 @@ export class HostUsagePricingCoordinator {
|
|
|
117
110
|
? { buckets: [...legacy], provenance: EMPTY_PROVENANCE }
|
|
118
111
|
: mergeUsageBuckets(legacy,
|
|
119
112
|
// Only the first page repairs; later pages reuse it.
|
|
120
|
-
await this.#
|
|
113
|
+
await readCanonicalUsageBuckets(this.#stores, input.query, input.groupBy, now, offset === 0));
|
|
121
114
|
if (offset > merged.buckets.length)
|
|
122
115
|
return invalidUsageOffset();
|
|
123
116
|
return {
|
|
@@ -142,7 +135,7 @@ export class HostUsagePricingCoordinator {
|
|
|
142
135
|
const legacy = await this.#stores.telemetry.logs(input.query, 0, offset + limit);
|
|
143
136
|
const merged = mergeUsageLogs(legacy,
|
|
144
137
|
// Only the first page repairs; later pages reuse it.
|
|
145
|
-
await this.#
|
|
138
|
+
await readCanonicalUsageLogs(this.#stores, input.query, now, offset + limit, offset === 0), offset, limit);
|
|
146
139
|
if (offset > merged.total)
|
|
147
140
|
return invalidUsageOffset();
|
|
148
141
|
const titles = await this.#resolveSessionTitles(merged.rows);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
20
|
import { WORKHUB_COORDINATION_SESSION_ID, isWorkHubCoordinationSessionTarget, } from '@maka/core/session';
|
|
21
|
-
import { readWorkHubRequestIntent, workHubCorrectionTargetsSession, workHubCreationAuthorizesTitle, } from '@maka/core/workhub-creation-intent';
|
|
21
|
+
import { readWorkHubRequestIntent, workHubCorrectionTargetsSession, workHubCreationAuthorizesTitle, workHubNamedDelegationActionTargetsSession, } from '@maka/core/workhub-creation-intent';
|
|
22
22
|
import { WORKHUB_COORDINATION_CANDIDATE_MAX_ITEMS } from '../protocol/index.js';
|
|
23
23
|
const SIDE_CONVERSATION_LABEL = 'mode:side_conversation';
|
|
24
24
|
const ACTION_REPLAY_MAX_ITEMS = 256;
|
|
@@ -74,9 +74,9 @@ export class WorkHubCoordinationActionGate {
|
|
|
74
74
|
const action = { requestFingerprint, result };
|
|
75
75
|
this.#actions.set(input.actionId, action);
|
|
76
76
|
// Successful actions remain a Host-lifetime fast path. Rejections release
|
|
77
|
-
// the slot so
|
|
78
|
-
//
|
|
79
|
-
//
|
|
77
|
+
// the slot so admission can retry. Durable identity belongs to the
|
|
78
|
+
// owning operation: action claims for coordination writes, Host Turn
|
|
79
|
+
// admission for resume.
|
|
80
80
|
void result.catch(() => {
|
|
81
81
|
if (this.#actions.get(input.actionId) === action) {
|
|
82
82
|
this.#actions.delete(input.actionId);
|
|
@@ -177,6 +177,26 @@ export class WorkHubCoordinationActionGate {
|
|
|
177
177
|
});
|
|
178
178
|
return this.#stop(requested, source);
|
|
179
179
|
}
|
|
180
|
+
if (proposal.disposition === 'resume_work') {
|
|
181
|
+
if (!requestIntent.resume.imperative) {
|
|
182
|
+
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub resume requires an explicit named command in trusted user text');
|
|
183
|
+
}
|
|
184
|
+
const candidates = await this.candidates();
|
|
185
|
+
const target = candidates.candidates.find((candidate) => candidate.sessionId === proposal.expects.targetSessionId);
|
|
186
|
+
if (!target)
|
|
187
|
+
throw new WorkHubActionGateFailure('candidate_unavailable', 'WorkHub resume target is unavailable');
|
|
188
|
+
this.#assertTarget(target);
|
|
189
|
+
const source = await this.#soleWorkingDelegation(target.sessionId, 'resume');
|
|
190
|
+
if (source.actionId !== proposal.resumesActionId) {
|
|
191
|
+
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub resume target delegation changed');
|
|
192
|
+
}
|
|
193
|
+
const currentTargetName = target.sessionName;
|
|
194
|
+
if (!currentTargetName ||
|
|
195
|
+
!workHubNamedDelegationActionTargetsSession(requestIntent.resume, currentTargetName)) {
|
|
196
|
+
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub resume target is not affirmed in trusted user text');
|
|
197
|
+
}
|
|
198
|
+
return this.#effects.resume({ actionId: input.actionId, source }, context);
|
|
199
|
+
}
|
|
180
200
|
if (proposal.disposition === 'create_new') {
|
|
181
201
|
if (!input.create || !workHubCreationAuthorizesTitle(requestIntent, proposal.title)) {
|
|
182
202
|
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub creation requires an unambiguous instruction and creation context');
|
|
@@ -259,9 +279,22 @@ export class WorkHubCoordinationActionGate {
|
|
|
259
279
|
return claimed;
|
|
260
280
|
}
|
|
261
281
|
}
|
|
282
|
+
const resolved = await this.#soleWorkingDelegation(targetSessionId, 'stop');
|
|
283
|
+
// A claim with no request behind it resolves from the active links like a
|
|
284
|
+
// first attempt, but only while those links still name the delegation it
|
|
285
|
+
// bound itself to. If that one left and another took its place, the
|
|
286
|
+
// fingerprint derived here would no longer match the claim, and since
|
|
287
|
+
// claims are never deleted the refusal would be permanent and unexplained.
|
|
288
|
+
// Say why instead: the identity is spent, and the retry needs a new one.
|
|
289
|
+
if (claim?.operation === 'stop' && resolved.delegationId !== claim.subject) {
|
|
290
|
+
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub stop identity is already bound to a different delegation');
|
|
291
|
+
}
|
|
292
|
+
return resolved;
|
|
293
|
+
}
|
|
294
|
+
async #soleWorkingDelegation(targetSessionId, operation) {
|
|
262
295
|
const onTarget = await this.#effects.listActiveAssignments(targetSessionId);
|
|
263
296
|
if (onTarget.length === 0) {
|
|
264
|
-
throw new WorkHubActionGateFailure('action_conflict',
|
|
297
|
+
throw new WorkHubActionGateFailure('action_conflict', `WorkHub has no active durable delegation to ${operation} on that Session`);
|
|
265
298
|
}
|
|
266
299
|
// One link is the answer whatever state its work is in. Whether that work
|
|
267
300
|
// finished, or was never WorkHub's to stop, is what the stop resolves to —
|
|
@@ -281,19 +314,10 @@ export class WorkHubCoordinationActionGate {
|
|
|
281
314
|
}
|
|
282
315
|
}
|
|
283
316
|
if (holdingWork.length !== 1) {
|
|
284
|
-
throw new WorkHubActionGateFailure('action_conflict',
|
|
317
|
+
throw new WorkHubActionGateFailure('action_conflict', `WorkHub ${operation} target does not identify one active durable delegation`);
|
|
285
318
|
}
|
|
286
319
|
resolved = holdingWork[0];
|
|
287
320
|
}
|
|
288
|
-
// A claim with no request behind it resolves from the active links like a
|
|
289
|
-
// first attempt, but only while those links still name the delegation it
|
|
290
|
-
// bound itself to. If that one left and another took its place, the
|
|
291
|
-
// fingerprint derived here would no longer match the claim, and since
|
|
292
|
-
// claims are never deleted the refusal would be permanent and unexplained.
|
|
293
|
-
// Say why instead: the identity is spent, and the retry needs a new one.
|
|
294
|
-
if (claim?.operation === 'stop' && resolved.delegationId !== claim.subject) {
|
|
295
|
-
throw new WorkHubActionGateFailure('action_conflict', 'WorkHub stop identity is already bound to a different delegation');
|
|
296
|
-
}
|
|
297
321
|
return resolved;
|
|
298
322
|
}
|
|
299
323
|
async #stop(request, source) {
|
|
@@ -608,6 +632,11 @@ function delegationAssignment(input, actionFingerprint, targetSessionId, targetS
|
|
|
608
632
|
function workHubCreatedSessionId(actionId) {
|
|
609
633
|
return `whs_${hash(`create\0${actionId}`).slice(0, 48)}`;
|
|
610
634
|
}
|
|
635
|
+
// One request cannot resume a later interruption after a lost response and restart.
|
|
636
|
+
// Host admission rejects reuse of this Turn id for a different source boundary.
|
|
637
|
+
export function workHubResumedTurnId(actionId) {
|
|
638
|
+
return `wht_${hash(`resume\0${actionId}`).slice(0, 48)}`;
|
|
639
|
+
}
|
|
611
640
|
function workspaceProjection(session) {
|
|
612
641
|
return {
|
|
613
642
|
target: typeof session.projectId === 'string'
|
|
@@ -110,6 +110,11 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
110
110
|
resolveStop: (input) => this.#resolveStop(input),
|
|
111
111
|
readDelegationRetirement: options.sessionActions.readDelegationRetirement,
|
|
112
112
|
retireDelegation: options.sessionActions.retireDelegation,
|
|
113
|
+
resume: async (input, context) => ({
|
|
114
|
+
disposition: 'resume_work',
|
|
115
|
+
targetSessionId: input.source.targetSessionId,
|
|
116
|
+
...(await options.sessionActions.resumeDelegation(input.source, context, input.actionId)),
|
|
117
|
+
}),
|
|
113
118
|
});
|
|
114
119
|
}
|
|
115
120
|
#prepareReplacement(input) {
|
|
@@ -342,7 +347,7 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
342
347
|
return {
|
|
343
348
|
ok: false,
|
|
344
349
|
error: {
|
|
345
|
-
code: error.code
|
|
350
|
+
code: error.code,
|
|
346
351
|
message: error.message,
|
|
347
352
|
},
|
|
348
353
|
};
|
|
@@ -258,6 +258,26 @@ class SqliteAgentRunStore {
|
|
|
258
258
|
if (readSqliteRootTurnStartRejection(this.#lease.database, admission.sessionId, admission.turnId)) {
|
|
259
259
|
throw new Error('Root Turn identity is already rejected');
|
|
260
260
|
}
|
|
261
|
+
if (admission.execution.kind === 'safe_boundary_continuation') {
|
|
262
|
+
const sourceOwner = this.#lease.database
|
|
263
|
+
.prepare(`
|
|
264
|
+
SELECT turn_id
|
|
265
|
+
FROM core_root_turn_admissions
|
|
266
|
+
WHERE session_id = ?
|
|
267
|
+
AND json_extract(record_json, '$.execution.sourceTurnId') = ?
|
|
268
|
+
AND json_extract(record_json, '$.execution.sourceRunId') = ?
|
|
269
|
+
AND json_extract(record_json, '$.execution.kind') = 'safe_boundary_continuation'
|
|
270
|
+
ORDER BY admitted_at, turn_id
|
|
271
|
+
LIMIT 1
|
|
272
|
+
`)
|
|
273
|
+
.get(admission.sessionId, admission.execution.sourceTurnId, admission.execution.sourceRunId);
|
|
274
|
+
if (typeof sourceOwner?.turn_id === 'string') {
|
|
275
|
+
const owner = readSqliteRootTurnAdmission(this.#lease.database, admission.sessionId, sourceOwner.turn_id);
|
|
276
|
+
if (!owner)
|
|
277
|
+
throw new Error('Root continuation index has no durable admission');
|
|
278
|
+
return { kind: 'conflict', admission: owner };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
261
281
|
for (const source of admission.sourceMessages) {
|
|
262
282
|
const proof = this.#lease.database
|
|
263
283
|
.prepare(`
|
|
@@ -293,6 +313,30 @@ class SqliteAgentRunStore {
|
|
|
293
313
|
assertSafeId(turnId, 'Invalid turn id');
|
|
294
314
|
return readSqliteRootTurnAdmission(this.#lease.database, sessionId, turnId);
|
|
295
315
|
}
|
|
316
|
+
async readRootTurnContinuationAdmission(sessionId, sourceTurnId, sourceRunId) {
|
|
317
|
+
assertSafeId(sessionId, 'Invalid session id');
|
|
318
|
+
assertSafeId(sourceTurnId, 'Invalid source turn id');
|
|
319
|
+
assertSafeId(sourceRunId, 'Invalid source run id');
|
|
320
|
+
const row = this.#lease.database
|
|
321
|
+
.prepare(`
|
|
322
|
+
SELECT turn_id, record_json
|
|
323
|
+
FROM core_root_turn_admissions
|
|
324
|
+
WHERE session_id = ?
|
|
325
|
+
AND json_extract(record_json, '$.execution.sourceTurnId') = ?
|
|
326
|
+
AND json_extract(record_json, '$.execution.sourceRunId') = ?
|
|
327
|
+
AND json_extract(record_json, '$.execution.kind') = 'safe_boundary_continuation'
|
|
328
|
+
ORDER BY admitted_at, turn_id
|
|
329
|
+
LIMIT 1
|
|
330
|
+
`)
|
|
331
|
+
.get(sessionId, sourceTurnId, sourceRunId);
|
|
332
|
+
if (!row)
|
|
333
|
+
return undefined;
|
|
334
|
+
if (typeof row.turn_id !== 'string' || typeof row.record_json !== 'string') {
|
|
335
|
+
throw new Error('Invalid SQLite root turn continuation admission row');
|
|
336
|
+
}
|
|
337
|
+
const admission = normalizeRootTurnAdmission(JSON.parse(row.record_json), sessionId, row.turn_id);
|
|
338
|
+
return admission;
|
|
339
|
+
}
|
|
296
340
|
async readRootTurnStartRejection(sessionId, turnId) {
|
|
297
341
|
assertSafeId(sessionId, 'Invalid session id');
|
|
298
342
|
assertSafeId(turnId, 'Invalid turn id');
|
|
@@ -67,6 +67,10 @@ class SqliteConversationOperationalStateStore {
|
|
|
67
67
|
database
|
|
68
68
|
.prepare('DELETE FROM core_root_turn_start_rejections WHERE session_id = ?')
|
|
69
69
|
.run(sessionId);
|
|
70
|
+
// Cascades this run's events and the Usage projection's checkpoints.
|
|
71
|
+
// `usage_model_call_attempts` is deliberately absent from this list:
|
|
72
|
+
// deleting a conversation must not erase its spend from all-time Usage
|
|
73
|
+
// totals, so those rows outlive the authority they were projected from.
|
|
70
74
|
database.prepare('DELETE FROM core_agent_runs WHERE session_id = ?').run(sessionId);
|
|
71
75
|
database
|
|
72
76
|
.prepare('DELETE FROM core_client_capability_session_grants WHERE session_id = ?')
|
|
@@ -210,6 +210,7 @@ async function createExecutionStoresForWrite(lease, kind, extension) {
|
|
|
210
210
|
repairEventProjection: (sessionId, type, event, options) => run(() => agentRunStore.repairEventProjection(sessionId, type, event, options)),
|
|
211
211
|
admitRootTurn: (input) => run(() => agentRunStore.admitRootTurn(input)),
|
|
212
212
|
readRootTurnAdmission: (sessionId, turnId) => run(() => agentRunStore.readRootTurnAdmission(sessionId, turnId)),
|
|
213
|
+
readRootTurnContinuationAdmission: (sessionId, sourceTurnId, sourceRunId) => run(() => agentRunStore.readRootTurnContinuationAdmission(sessionId, sourceTurnId, sourceRunId)),
|
|
213
214
|
readRootTurnStartRejection: (sessionId, turnId) => run(() => agentRunStore.readRootTurnStartRejection(sessionId, turnId)),
|
|
214
215
|
commitRootTurnStartRejection: (input) => run(() => agentRunStore.commitRootTurnStartRejection(input)),
|
|
215
216
|
readRootTurnSourceMessageReceipt: (sessionId, sourceMessageId) => run(() => agentRunStore.readRootTurnSourceMessageReceipt(sessionId, sourceMessageId)),
|
|
@@ -302,6 +303,7 @@ async function openExecutionStoresForRead(lease, kind, extension) {
|
|
|
302
303
|
readEventsByTypeBounded: (sessionId, runId, type, budget) => run(() => agentRunStore.readEventsByTypeBounded(sessionId, runId, type, budget)),
|
|
303
304
|
readEventProjection: (sessionId, type) => run(() => agentRunStore.readEventProjection(sessionId, type)),
|
|
304
305
|
readRootTurnAdmission: (sessionId, turnId) => run(() => agentRunStore.readRootTurnAdmission(sessionId, turnId)),
|
|
306
|
+
readRootTurnContinuationAdmission: (sessionId, sourceTurnId, sourceRunId) => run(() => agentRunStore.readRootTurnContinuationAdmission(sessionId, sourceTurnId, sourceRunId)),
|
|
305
307
|
readRootTurnSourceMessageReceipt: (sessionId, sourceMessageId) => run(() => agentRunStore.readRootTurnSourceMessageReceipt(sessionId, sourceMessageId)),
|
|
306
308
|
},
|
|
307
309
|
runtimeEventStore: {
|