newmark-agent 0.6.2 → 0.6.3
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/dist/conversation-utility-host.bundle.cjs +1947 -470
- package/dist/core/agent.d.ts +14 -0
- package/dist/core/agent.js +44 -3
- package/dist/core/agentKernelRunner.js +24 -2
- package/dist/core/continuation/contracts.d.ts +197 -0
- package/dist/core/continuation/contracts.js +72 -0
- package/dist/core/continuation/store.d.ts +178 -0
- package/dist/core/continuation/store.js +989 -0
- package/dist/core/conversationKernel.d.ts +80 -1
- package/dist/core/conversationKernel.js +586 -36
- package/dist/core/requestContextEstimate.d.ts +5 -1
- package/dist/llm/provider.js +9 -2
- package/dist/main.js +39 -4
- package/dist/providers/chat-completions.adapter.js +6 -1
- package/dist/providers/responses.adapter.js +4 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/dist/ui/index.html +70 -11
- package/dist/wsl-agent-host.bundle.cjs +1950 -473
- package/package.json +3 -2
package/dist/core/agent.d.ts
CHANGED
|
@@ -294,8 +294,15 @@ export interface ConversationBranchGroupSnapshot {
|
|
|
294
294
|
export interface ConversationContinuation {
|
|
295
295
|
content: string;
|
|
296
296
|
queueMode: 'steer' | 'followUp';
|
|
297
|
+
buildId?: string;
|
|
297
298
|
clientMessageId?: string;
|
|
298
299
|
runId?: string;
|
|
300
|
+
branchNodeId?: string;
|
|
301
|
+
branchPath?: string[];
|
|
302
|
+
ledgerBranchId?: string;
|
|
303
|
+
targetRuntimeKey?: string;
|
|
304
|
+
workspaceKey?: string;
|
|
305
|
+
modelSelection?: string;
|
|
299
306
|
images?: Array<{
|
|
300
307
|
dataUrl: string;
|
|
301
308
|
name?: string;
|
|
@@ -787,6 +794,13 @@ export declare class Agent {
|
|
|
787
794
|
*/
|
|
788
795
|
private deriveConversationTitleFromProvider;
|
|
789
796
|
private conversationTitleFailureCause;
|
|
797
|
+
/**
|
|
798
|
+
* Deterministic provider rejections (bad key, forbidden endpoint, unknown
|
|
799
|
+
* model, invalid request, payment required) cannot succeed by retrying the
|
|
800
|
+
* identical title probe. Only transient transport, timeout, rate-limit and
|
|
801
|
+
* provider-internal failures enter the bounded 5-stage backoff.
|
|
802
|
+
*/
|
|
803
|
+
private conversationTitleFailureIsRetryable;
|
|
790
804
|
private waitForConversationTitleRetry;
|
|
791
805
|
/** Legacy completion hook retained as a no-op; naming is pre-response only. */
|
|
792
806
|
private maybeAutoRenameConversationFromRun;
|
package/dist/core/agent.js
CHANGED
|
@@ -2735,8 +2735,15 @@ class Agent {
|
|
|
2735
2735
|
deduped.set(key, {
|
|
2736
2736
|
content,
|
|
2737
2737
|
queueMode,
|
|
2738
|
+
buildId: String(raw.buildId || '').trim() || undefined,
|
|
2738
2739
|
clientMessageId,
|
|
2739
2740
|
runId: String(raw.runId || '').trim() || undefined,
|
|
2741
|
+
branchNodeId: String(raw.branchNodeId || '').trim() || undefined,
|
|
2742
|
+
branchPath: Array.isArray(raw.branchPath) ? raw.branchPath.map(id => String(id || '')).filter(Boolean) : undefined,
|
|
2743
|
+
ledgerBranchId: String(raw.ledgerBranchId || '').trim() || undefined,
|
|
2744
|
+
targetRuntimeKey: String(raw.targetRuntimeKey || '').trim() || undefined,
|
|
2745
|
+
workspaceKey: String(raw.workspaceKey || '').trim() || undefined,
|
|
2746
|
+
modelSelection: String(raw.modelSelection || '').trim() || undefined,
|
|
2740
2747
|
images: images.length ? images : undefined,
|
|
2741
2748
|
attachments: attachments.length ? attachments : undefined,
|
|
2742
2749
|
hiddenUserInput: raw.hiddenUserInput === true,
|
|
@@ -3950,6 +3957,19 @@ class Agent {
|
|
|
3950
3957
|
return 'provider returned invalid JSON';
|
|
3951
3958
|
return 'provider request failed';
|
|
3952
3959
|
}
|
|
3960
|
+
/**
|
|
3961
|
+
* Deterministic provider rejections (bad key, forbidden endpoint, unknown
|
|
3962
|
+
* model, invalid request, payment required) cannot succeed by retrying the
|
|
3963
|
+
* identical title probe. Only transient transport, timeout, rate-limit and
|
|
3964
|
+
* provider-internal failures enter the bounded 5-stage backoff.
|
|
3965
|
+
*/
|
|
3966
|
+
conversationTitleFailureIsRetryable(error) {
|
|
3967
|
+
const message = error instanceof Error ? error.message : '';
|
|
3968
|
+
const status = message.match(/^\[LLM Error:\s*([1-5]\d{2})\]/)?.[1];
|
|
3969
|
+
if (!status)
|
|
3970
|
+
return true;
|
|
3971
|
+
return status === '408' || status === '429' || status.startsWith('5');
|
|
3972
|
+
}
|
|
3953
3973
|
async waitForConversationTitleRetry(delayMs, signal) {
|
|
3954
3974
|
if (signal?.aborted || delayMs <= 0)
|
|
3955
3975
|
return;
|
|
@@ -3981,17 +4001,20 @@ class Agent {
|
|
|
3981
4001
|
// Failure state belongs to this title attempt sequence, not the Agent or
|
|
3982
4002
|
// another conversation. A later empty title replaces an earlier HTTP error.
|
|
3983
4003
|
let lastFailure = 'provider returned an empty or unusable title';
|
|
4004
|
+
let terminalFailure = false;
|
|
3984
4005
|
const requestTitle = async () => {
|
|
3985
4006
|
lastFailure = 'provider returned an empty or unusable title';
|
|
4007
|
+
terminalFailure = false;
|
|
3986
4008
|
try {
|
|
3987
4009
|
return await this.deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal);
|
|
3988
4010
|
}
|
|
3989
4011
|
catch (error) {
|
|
3990
4012
|
lastFailure = this.conversationTitleFailureCause(error, provider);
|
|
4013
|
+
terminalFailure = !this.conversationTitleFailureIsRetryable(error);
|
|
3991
4014
|
return '';
|
|
3992
4015
|
}
|
|
3993
4016
|
};
|
|
3994
|
-
const exhaustedFailure = () => new Error(`Conversation title generation failed; ${lastFailure}. The first Agent request was not started. Retry the first input.`);
|
|
4017
|
+
const exhaustedFailure = () => new Error(`Conversation title generation failed for ${provider.name}/${modelName}; ${lastFailure}. The first Agent request was not started. Retry the first input or switch the model.`);
|
|
3995
4018
|
if (!stateKey) {
|
|
3996
4019
|
// Pure Agent/CLI mode has no workspace conversation file to rename, but
|
|
3997
4020
|
// still uses the title request as the required first model-availability
|
|
@@ -3999,6 +4022,8 @@ class Agent {
|
|
|
3999
4022
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
4000
4023
|
if (await requestTitle())
|
|
4001
4024
|
return true;
|
|
4025
|
+
if (terminalFailure)
|
|
4026
|
+
break;
|
|
4002
4027
|
if (signal?.aborted)
|
|
4003
4028
|
return false;
|
|
4004
4029
|
if (attempt < maxAttempts - 1)
|
|
@@ -4036,6 +4061,8 @@ class Agent {
|
|
|
4036
4061
|
this.publishConversationTitle(title, conversationId);
|
|
4037
4062
|
return true;
|
|
4038
4063
|
}
|
|
4064
|
+
if (terminalFailure)
|
|
4065
|
+
break;
|
|
4039
4066
|
if (signal?.aborted)
|
|
4040
4067
|
return false;
|
|
4041
4068
|
if (attempt < maxAttempts - 1)
|
|
@@ -6015,7 +6042,7 @@ class Agent {
|
|
|
6015
6042
|
let requestContext = null;
|
|
6016
6043
|
const context = saved?.version === 1 ? saved.requestContext : null;
|
|
6017
6044
|
if (context && typeof context.requestId === 'string' && typeof context.runId === 'string' && typeof context.at === 'string') {
|
|
6018
|
-
requestContext = { requestId: context.requestId, runId: context.runId, model: String(context.model || ''), at: context.at,
|
|
6045
|
+
requestContext = { requestId: context.requestId, runId: context.runId, branchId: String(context.branchId || ''), contextHash: String(context.contextHash || ''), model: String(context.model || ''), at: context.at,
|
|
6019
6046
|
estimatedTokens: 0, longHistoryTokens: 0, buildBlockTokens: 0, systemPromptTokens: 0, toolSchemaTokens: 0, messageCount: 0, hasImages: context.hasImages === true };
|
|
6020
6047
|
for (const key of ['estimatedTokens', 'longHistoryTokens', 'buildBlockTokens', 'systemPromptTokens', 'toolSchemaTokens', 'messageCount', 'inputTokens', 'cacheReadTokens']) {
|
|
6021
6048
|
const value = context[key];
|
|
@@ -6083,9 +6110,16 @@ class Agent {
|
|
|
6083
6110
|
return request;
|
|
6084
6111
|
}
|
|
6085
6112
|
recordRequestContext(request, messages, system, tools, model) {
|
|
6113
|
+
const contextHash = crypto.createHash('sha256').update(JSON.stringify({
|
|
6114
|
+
model,
|
|
6115
|
+
system,
|
|
6116
|
+
tools,
|
|
6117
|
+
messages,
|
|
6118
|
+
})).digest('hex').slice(0, 24);
|
|
6086
6119
|
const context = {
|
|
6087
6120
|
...(0, requestContextEstimate_1.estimateSubmittedContext)(messages, system, tools, this.compressionBuildBlockStart(messages)),
|
|
6088
|
-
requestId: request.id, runId: this.currentWorkRunId(),
|
|
6121
|
+
requestId: request.id, runId: this.currentWorkRunId(), branchId: this.currentBranchNodeId(),
|
|
6122
|
+
contextHash, model, at: new Date().toISOString(),
|
|
6089
6123
|
};
|
|
6090
6124
|
this.mutateRequestUsage(request, usage => { usage.requestContext = context; });
|
|
6091
6125
|
}
|
|
@@ -8187,6 +8221,13 @@ class Agent {
|
|
|
8187
8221
|
if (!titleProvider || !titleModelName) {
|
|
8188
8222
|
throw new Error('Conversation title generation failed; no resolved model deployment is available. No LLM configured. Add provider in Settings > Models.');
|
|
8189
8223
|
}
|
|
8224
|
+
// The title probe is a hard gate before the first formal request. Show
|
|
8225
|
+
// the exact deployment being checked immediately so a slow or failing
|
|
8226
|
+
// first turn never looks like an unexplained freeze.
|
|
8227
|
+
this.emitWorkEvent({
|
|
8228
|
+
type: 'status',
|
|
8229
|
+
content: `Preparing the first response: checking ${titleProvider.name}/${titleModelName} and generating the conversation title.`,
|
|
8230
|
+
});
|
|
8190
8231
|
const titled = await this.startFirstInputConversationTitle(firstResponseTitleInput.messageId, firstResponseTitleInput.input, titleProvider, titleModelName, titleIntelligence, processSignal);
|
|
8191
8232
|
if (!titled)
|
|
8192
8233
|
throw new Error('Conversation title generation failed; the first Agent request was not started. Retry the first input.');
|
|
@@ -69,6 +69,24 @@ const HIDDEN_LINE_PREFIXES = [
|
|
|
69
69
|
'analysis:',
|
|
70
70
|
'analysis:',
|
|
71
71
|
];
|
|
72
|
+
/**
|
|
73
|
+
* Optional provider-side session cache identity. A remote session is mutable
|
|
74
|
+
* state, so it must never be shared by two branches of the same conversation:
|
|
75
|
+
* a fork that reuses the root conversation id can read the other branch's
|
|
76
|
+
* history. Keep the id stable within one branch (normal sequential turns still
|
|
77
|
+
* cache-hit) and change it the moment the runtime branch changes.
|
|
78
|
+
*/
|
|
79
|
+
function providerSessionIdentity(agent) {
|
|
80
|
+
const conversationId = String(agent.activeConversationId || 'default');
|
|
81
|
+
try {
|
|
82
|
+
const snapshot = agent.getConversationSnapshot(conversationId);
|
|
83
|
+
const branchId = String(snapshot.runtimeBranchId || snapshot.activeBranchId || '');
|
|
84
|
+
return branchId ? `${conversationId}::branch:${branchId}` : conversationId;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return conversationId;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
72
90
|
function partialMarkerSuffix(value, marker) {
|
|
73
91
|
const lower = value.toLowerCase();
|
|
74
92
|
const target = marker.toLowerCase();
|
|
@@ -660,7 +678,11 @@ async function runAgentKernel(agent) {
|
|
|
660
678
|
currentAgent.checkpointPeerInput();
|
|
661
679
|
peerInputCheckpointed = true;
|
|
662
680
|
}
|
|
663
|
-
const { temperature,
|
|
681
|
+
const { temperature, reasoningEffort } = currentProvider.intelligenceConfig(currentAgent.intelligence);
|
|
682
|
+
// A normal Agent reply has no client-side output cap. The provider
|
|
683
|
+
// endpoint owns its real limit; protocols that require a positive
|
|
684
|
+
// max_tokens resolve one internally.
|
|
685
|
+
const maxTokens = 0;
|
|
664
686
|
const newmarkMessages = fromKernelMessages(context.messages).map(message => message.role === 'assistant'
|
|
665
687
|
// Use the same public-content boundary on its first provider
|
|
666
688
|
// submission and after persistence. Trimming only the durable
|
|
@@ -697,7 +719,7 @@ async function runAgentKernel(agent) {
|
|
|
697
719
|
});
|
|
698
720
|
}
|
|
699
721
|
for await (const token of currentProvider.chatStreamWithTools(currentModelName, newmarkMessages, requestSystemPrompt, temperature, maxTokens, providerTools, options?.signal, reasoningEffort, currentAgent.config.getBool('context', 'provider_session_id')
|
|
700
|
-
? currentAgent
|
|
722
|
+
? providerSessionIdentity(currentAgent)
|
|
701
723
|
: undefined)) {
|
|
702
724
|
if (!firstTokenRecorded && ((token.type === 'text' && token.text) || (token.type === 'tool_call' && token.toolCall))) {
|
|
703
725
|
firstTokenRecorded = true;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* continuation-v1 contracts.
|
|
3
|
+
*
|
|
4
|
+
* These are the runtime-checked boundaries shared by the PC GUI, the hosted
|
|
5
|
+
* mobile API and the isolated runtimes. A view may read a snapshot, but it can
|
|
6
|
+
* never supply the execution identity that a write is allowed to use.
|
|
7
|
+
*/
|
|
8
|
+
export type WorkspaceId = string;
|
|
9
|
+
export type RootId = string;
|
|
10
|
+
export type BranchId = string;
|
|
11
|
+
export type BuildId = string;
|
|
12
|
+
export type AttemptId = string;
|
|
13
|
+
export type CommandId = string;
|
|
14
|
+
export type SnapshotId = string;
|
|
15
|
+
export declare const CONTINUATION_SCHEMA_VERSION: 1;
|
|
16
|
+
export type BuildStatus = 'QUEUED' | 'RUNNING' | 'WAITING_TOOL' | 'WAITING_APPROVAL' | 'WAITING_SUBAGENT' | 'FINALIZING' | 'SUCCEEDED' | 'FAILED' | 'CANCELLED';
|
|
17
|
+
export type AttemptStatus = 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'EXPIRED';
|
|
18
|
+
export type ContinuationErrorCode = 'TARGET_REQUIRED' | 'ROUTE_MISMATCH' | 'QUEUE_REVISION_CONFLICT' | 'ANCHOR_NOT_IN_LINEAGE' | 'ANCHOR_NOT_COMMITTED' | 'IDEMPOTENCY_KEY_REUSE' | 'STALE_EXECUTION' | 'FINAL_ALREADY_COMMITTED' | 'BUILD_NOT_READY' | 'BRANCH_PAUSED' | 'EFFECT_UNCERTAIN' | 'UNKNOWN_BRANCH' | 'UNKNOWN_BUILD' | 'UNKNOWN_COMMAND';
|
|
19
|
+
export declare class ContinuationError extends Error {
|
|
20
|
+
readonly code: ContinuationErrorCode;
|
|
21
|
+
readonly detail?: Record<string, unknown> | undefined;
|
|
22
|
+
constructor(code: ContinuationErrorCode, message: string, detail?: Record<string, unknown> | undefined);
|
|
23
|
+
}
|
|
24
|
+
export interface CommandTarget {
|
|
25
|
+
workspaceId: WorkspaceId;
|
|
26
|
+
expectedRootId: RootId;
|
|
27
|
+
branchId: BranchId;
|
|
28
|
+
}
|
|
29
|
+
export interface BuildInput {
|
|
30
|
+
text: string;
|
|
31
|
+
images?: Array<{
|
|
32
|
+
dataUrl: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
type?: string;
|
|
35
|
+
}>;
|
|
36
|
+
attachments?: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
mimeType: string;
|
|
40
|
+
dataUrl?: string;
|
|
41
|
+
}>;
|
|
42
|
+
requestedMode?: string;
|
|
43
|
+
goalObjective?: string;
|
|
44
|
+
/** Provider-qualified model frozen at command acceptance time. */
|
|
45
|
+
modelSelection?: string;
|
|
46
|
+
/** Branch node captured when the user submitted the command. */
|
|
47
|
+
branchNodeId?: string;
|
|
48
|
+
branchPath?: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface EnqueueBuildCommand {
|
|
51
|
+
schemaVersion: typeof CONTINUATION_SCHEMA_VERSION;
|
|
52
|
+
type: 'ENQUEUE_BUILD';
|
|
53
|
+
commandId: CommandId;
|
|
54
|
+
actorId: string;
|
|
55
|
+
target: CommandTarget;
|
|
56
|
+
expectedTailBuildId: BuildId | null;
|
|
57
|
+
expectedQueueRevision: number;
|
|
58
|
+
input: BuildInput;
|
|
59
|
+
recipeSnapshotId: SnapshotId;
|
|
60
|
+
createdAt: string;
|
|
61
|
+
}
|
|
62
|
+
export interface CommandReceipt {
|
|
63
|
+
schemaVersion: typeof CONTINUATION_SCHEMA_VERSION;
|
|
64
|
+
commandId: CommandId;
|
|
65
|
+
requestHash: string;
|
|
66
|
+
workspaceId: WorkspaceId;
|
|
67
|
+
rootId: RootId;
|
|
68
|
+
branchId: BranchId;
|
|
69
|
+
buildId: BuildId;
|
|
70
|
+
parentBuildId: BuildId | null;
|
|
71
|
+
queueSequence: number;
|
|
72
|
+
queueRevision: number;
|
|
73
|
+
lifecycleRevision: number;
|
|
74
|
+
acceptedAt: string;
|
|
75
|
+
duplicate?: boolean;
|
|
76
|
+
}
|
|
77
|
+
export interface ExecutionScope {
|
|
78
|
+
workspaceId: WorkspaceId;
|
|
79
|
+
rootId: RootId;
|
|
80
|
+
branchId: BranchId;
|
|
81
|
+
buildId: BuildId;
|
|
82
|
+
attemptId: AttemptId;
|
|
83
|
+
fence: number;
|
|
84
|
+
parentBuildId: BuildId | null;
|
|
85
|
+
baseContextSnapshotId: SnapshotId;
|
|
86
|
+
environmentSnapshotId: SnapshotId;
|
|
87
|
+
workerCapability: string;
|
|
88
|
+
}
|
|
89
|
+
export interface BranchGuardSnapshot {
|
|
90
|
+
branchId: BranchId;
|
|
91
|
+
activeBuildId: BuildId | null;
|
|
92
|
+
activeAttemptId: AttemptId | null;
|
|
93
|
+
fence: number;
|
|
94
|
+
leaseUntil: number;
|
|
95
|
+
workerCapability: string;
|
|
96
|
+
}
|
|
97
|
+
export interface QueueSnapshot {
|
|
98
|
+
workspaceId: WorkspaceId;
|
|
99
|
+
rootId: RootId;
|
|
100
|
+
branchId: BranchId;
|
|
101
|
+
headBuildId: BuildId | null;
|
|
102
|
+
tailBuildId: BuildId | null;
|
|
103
|
+
queueRevision: number;
|
|
104
|
+
lifecycleRevision: number;
|
|
105
|
+
paused: boolean;
|
|
106
|
+
activeBuildId: BuildId | null;
|
|
107
|
+
guard: BranchGuardSnapshot;
|
|
108
|
+
builds: BuildRecord[];
|
|
109
|
+
}
|
|
110
|
+
export interface BuildRecord {
|
|
111
|
+
buildId: BuildId;
|
|
112
|
+
workspaceId: WorkspaceId;
|
|
113
|
+
rootId: RootId;
|
|
114
|
+
branchId: BranchId;
|
|
115
|
+
parentBuildId: BuildId | null;
|
|
116
|
+
inputHash: string;
|
|
117
|
+
input: BuildInput;
|
|
118
|
+
recipeSnapshotId: SnapshotId;
|
|
119
|
+
status: BuildStatus;
|
|
120
|
+
queueSequence: number;
|
|
121
|
+
createdAt: string;
|
|
122
|
+
updatedAt: string;
|
|
123
|
+
finalId?: string;
|
|
124
|
+
waitingReason?: string;
|
|
125
|
+
}
|
|
126
|
+
export interface AttemptRecord {
|
|
127
|
+
attemptId: AttemptId;
|
|
128
|
+
buildId: BuildId;
|
|
129
|
+
branchId: BranchId;
|
|
130
|
+
fence: number;
|
|
131
|
+
status: AttemptStatus;
|
|
132
|
+
leaseUntil: number;
|
|
133
|
+
workerCapability: string;
|
|
134
|
+
startedAt: string;
|
|
135
|
+
endedAt?: string;
|
|
136
|
+
}
|
|
137
|
+
export interface FinalRecord {
|
|
138
|
+
finalId: string;
|
|
139
|
+
buildId: BuildId;
|
|
140
|
+
branchId: BranchId;
|
|
141
|
+
finalHash: string;
|
|
142
|
+
stateSnapshotId: SnapshotId;
|
|
143
|
+
text: string;
|
|
144
|
+
committedAt: string;
|
|
145
|
+
}
|
|
146
|
+
export interface ContinuationEvent {
|
|
147
|
+
eventId: string;
|
|
148
|
+
cursor: number;
|
|
149
|
+
type: 'BuildAccepted' | 'AttemptStarted' | 'AttemptHeartbeat' | 'BuildCommitted' | 'BuildFailed' | 'BuildCancelled' | 'BranchForked' | 'BranchPaused' | 'BranchResumed' | 'StaleWriteRejected';
|
|
150
|
+
workspaceId: WorkspaceId;
|
|
151
|
+
rootId: RootId;
|
|
152
|
+
branchId: BranchId;
|
|
153
|
+
buildId: BuildId | null;
|
|
154
|
+
attemptId: AttemptId | null;
|
|
155
|
+
fence: number | null;
|
|
156
|
+
at: string;
|
|
157
|
+
payload?: Record<string, unknown>;
|
|
158
|
+
}
|
|
159
|
+
export interface LedgerFile {
|
|
160
|
+
version: typeof CONTINUATION_SCHEMA_VERSION;
|
|
161
|
+
workspaceId: WorkspaceId;
|
|
162
|
+
roots: Record<RootId, {
|
|
163
|
+
rootId: RootId;
|
|
164
|
+
workspaceId: WorkspaceId;
|
|
165
|
+
defaultBranchId: BranchId;
|
|
166
|
+
createdAt: string;
|
|
167
|
+
}>;
|
|
168
|
+
branches: Record<BranchId, BranchRecord>;
|
|
169
|
+
builds: Record<BuildId, BuildRecord>;
|
|
170
|
+
attempts: Record<AttemptId, AttemptRecord>;
|
|
171
|
+
guards: Record<BranchId, BranchGuardSnapshot>;
|
|
172
|
+
commands: Record<string, CommandReceipt>;
|
|
173
|
+
finals: Record<BuildId, FinalRecord>;
|
|
174
|
+
events: ContinuationEvent[];
|
|
175
|
+
nextEventCursor: number;
|
|
176
|
+
nextQueueSequence: number;
|
|
177
|
+
}
|
|
178
|
+
export interface BranchRecord {
|
|
179
|
+
branchId: BranchId;
|
|
180
|
+
workspaceId: WorkspaceId;
|
|
181
|
+
rootId: RootId;
|
|
182
|
+
parentAnchorBuildId: BuildId | null;
|
|
183
|
+
headBuildId: BuildId | null;
|
|
184
|
+
tailBuildId: BuildId | null;
|
|
185
|
+
queueRevision: number;
|
|
186
|
+
lifecycleRevision: number;
|
|
187
|
+
paused: boolean;
|
|
188
|
+
createdAt: string;
|
|
189
|
+
updatedAt: string;
|
|
190
|
+
}
|
|
191
|
+
export declare function stableJson(value: unknown): string;
|
|
192
|
+
export declare function emptyLedger(workspaceId: WorkspaceId): LedgerFile;
|
|
193
|
+
export declare function defaultGuard(branchId: BranchId): BranchGuardSnapshot;
|
|
194
|
+
export declare function commandKey(actorId: string, commandId: CommandId): string;
|
|
195
|
+
export declare function buildInputHash(input: BuildInput): string;
|
|
196
|
+
export declare function requestHash(command: EnqueueBuildCommand): string;
|
|
197
|
+
//# sourceMappingURL=contracts.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* continuation-v1 contracts.
|
|
4
|
+
*
|
|
5
|
+
* These are the runtime-checked boundaries shared by the PC GUI, the hosted
|
|
6
|
+
* mobile API and the isolated runtimes. A view may read a snapshot, but it can
|
|
7
|
+
* never supply the execution identity that a write is allowed to use.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ContinuationError = exports.CONTINUATION_SCHEMA_VERSION = void 0;
|
|
11
|
+
exports.stableJson = stableJson;
|
|
12
|
+
exports.emptyLedger = emptyLedger;
|
|
13
|
+
exports.defaultGuard = defaultGuard;
|
|
14
|
+
exports.commandKey = commandKey;
|
|
15
|
+
exports.buildInputHash = buildInputHash;
|
|
16
|
+
exports.requestHash = requestHash;
|
|
17
|
+
exports.CONTINUATION_SCHEMA_VERSION = 1;
|
|
18
|
+
class ContinuationError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
detail;
|
|
21
|
+
constructor(code, message, detail) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.detail = detail;
|
|
25
|
+
this.name = 'ContinuationError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.ContinuationError = ContinuationError;
|
|
29
|
+
function stableJson(value) {
|
|
30
|
+
if (value === null || typeof value !== 'object')
|
|
31
|
+
return JSON.stringify(value);
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return `[${value.map(item => stableJson(item)).join(',')}]`;
|
|
34
|
+
const record = value;
|
|
35
|
+
return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')}}`;
|
|
36
|
+
}
|
|
37
|
+
function emptyLedger(workspaceId) {
|
|
38
|
+
return {
|
|
39
|
+
version: exports.CONTINUATION_SCHEMA_VERSION,
|
|
40
|
+
workspaceId,
|
|
41
|
+
roots: {},
|
|
42
|
+
branches: {},
|
|
43
|
+
builds: {},
|
|
44
|
+
attempts: {},
|
|
45
|
+
guards: {},
|
|
46
|
+
commands: {},
|
|
47
|
+
finals: {},
|
|
48
|
+
events: [],
|
|
49
|
+
nextEventCursor: 1,
|
|
50
|
+
nextQueueSequence: 1,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function defaultGuard(branchId) {
|
|
54
|
+
return { branchId, activeBuildId: null, activeAttemptId: null, fence: 0, leaseUntil: 0, workerCapability: '' };
|
|
55
|
+
}
|
|
56
|
+
function commandKey(actorId, commandId) {
|
|
57
|
+
return `${actorId}\u0000${commandId}`;
|
|
58
|
+
}
|
|
59
|
+
function buildInputHash(input) {
|
|
60
|
+
return require('crypto').createHash('sha256').update(stableJson(input)).digest('hex').slice(0, 24);
|
|
61
|
+
}
|
|
62
|
+
function requestHash(command) {
|
|
63
|
+
return require('crypto').createHash('sha256').update(stableJson({
|
|
64
|
+
type: command.type,
|
|
65
|
+
target: command.target,
|
|
66
|
+
expectedTailBuildId: command.expectedTailBuildId,
|
|
67
|
+
expectedQueueRevision: command.expectedQueueRevision,
|
|
68
|
+
input: command.input,
|
|
69
|
+
recipeSnapshotId: command.recipeSnapshotId,
|
|
70
|
+
})).digest('hex');
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { BranchId, BranchRecord, BuildId, BuildInput, BuildRecord, CommandReceipt, ContinuationEvent, EnqueueBuildCommand, ExecutionScope, FinalRecord, QueueSnapshot, RootId, SnapshotId } from './contracts';
|
|
2
|
+
export interface GuardedContinuationStoreOptions {
|
|
3
|
+
/** Lease duration for one attempt. */
|
|
4
|
+
leaseMs?: number;
|
|
5
|
+
/** Maximum retained append-only events per workspace ledger. */
|
|
6
|
+
eventRetention?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Single-writer continuation ledger for one workspace.
|
|
10
|
+
*
|
|
11
|
+
* The spec's reference uses SQLite. This implementation keeps the same
|
|
12
|
+
* boundaries with a lock-file + atomic-replace JSON ledger because the project
|
|
13
|
+
* does not ship a native SQLite module and an Electron native dependency would
|
|
14
|
+
* change the packaging/signing contract. All continuation writes must go
|
|
15
|
+
* through this class; callers never mutate the ledger directly.
|
|
16
|
+
*/
|
|
17
|
+
export declare class GuardedContinuationStore {
|
|
18
|
+
private readonly workspacePath;
|
|
19
|
+
private readonly workspaceId;
|
|
20
|
+
private readonly ledgerPath;
|
|
21
|
+
private readonly lockPath;
|
|
22
|
+
private readonly leaseMs;
|
|
23
|
+
private readonly eventRetention;
|
|
24
|
+
private fingerprint;
|
|
25
|
+
private cached;
|
|
26
|
+
constructor(workspacePath: string, workspaceId: string, options?: GuardedContinuationStoreOptions);
|
|
27
|
+
get filePath(): string;
|
|
28
|
+
ensureRoot(rootId: RootId, defaultBranchId: BranchId): void;
|
|
29
|
+
ensureBranch(input: {
|
|
30
|
+
rootId: RootId;
|
|
31
|
+
branchId: BranchId;
|
|
32
|
+
parentAnchorBuildId: BuildId | null;
|
|
33
|
+
}): BranchRecord;
|
|
34
|
+
getReceipt(actorId: string, commandId: string): CommandReceipt | undefined;
|
|
35
|
+
enqueueBuild(command: EnqueueBuildCommand): CommandReceipt;
|
|
36
|
+
claimBuild(input: {
|
|
37
|
+
workspaceId: string;
|
|
38
|
+
rootId: RootId;
|
|
39
|
+
branchId: BranchId;
|
|
40
|
+
workerCapability: string;
|
|
41
|
+
now?: number;
|
|
42
|
+
}): ExecutionScope | null;
|
|
43
|
+
updateQueuedBuild(input: {
|
|
44
|
+
workspaceId: string;
|
|
45
|
+
rootId: RootId;
|
|
46
|
+
branchId: BranchId;
|
|
47
|
+
buildId: BuildId;
|
|
48
|
+
input: BuildInput;
|
|
49
|
+
}): BuildRecord;
|
|
50
|
+
deleteQueuedBuild(input: {
|
|
51
|
+
workspaceId: string;
|
|
52
|
+
rootId: RootId;
|
|
53
|
+
branchId: BranchId;
|
|
54
|
+
buildId: BuildId;
|
|
55
|
+
}): void;
|
|
56
|
+
reorderQueuedBuilds(input: {
|
|
57
|
+
workspaceId: string;
|
|
58
|
+
rootId: RootId;
|
|
59
|
+
branchId: BranchId;
|
|
60
|
+
orderedBuildIds: BuildId[];
|
|
61
|
+
}): QueueSnapshot;
|
|
62
|
+
heartbeat(scope: Pick<ExecutionScope, 'branchId' | 'buildId' | 'attemptId' | 'fence'>, now?: number): boolean;
|
|
63
|
+
commitFinal(input: {
|
|
64
|
+
scope: ExecutionScope;
|
|
65
|
+
text: string;
|
|
66
|
+
finalHash?: string;
|
|
67
|
+
stateSnapshotId?: SnapshotId;
|
|
68
|
+
now?: number;
|
|
69
|
+
}): FinalRecord;
|
|
70
|
+
/**
|
|
71
|
+
* Trusted external-owner completion.
|
|
72
|
+
*
|
|
73
|
+
* A Flow is a kernel-owned external run, not a leased model worker. It still
|
|
74
|
+
* has to be a durable build so queued Goal/Next successors have a committed
|
|
75
|
+
* parent. This path validates the parent/head relation transactionally but
|
|
76
|
+
* does not require a BranchGuard attempt; the kernel owns the Flow lifetime.
|
|
77
|
+
* If the build is not the next ready build, it stays QUEUED and the caller
|
|
78
|
+
* must retry after the predecessor commits.
|
|
79
|
+
*/
|
|
80
|
+
commitExternalBuild(input: {
|
|
81
|
+
workspaceId: string;
|
|
82
|
+
rootId: RootId;
|
|
83
|
+
branchId: BranchId;
|
|
84
|
+
buildId: BuildId;
|
|
85
|
+
text?: string;
|
|
86
|
+
now?: number;
|
|
87
|
+
}): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Claim a new attempt for an existing external Flow build (suspend/resume).
|
|
90
|
+
* The build identity and fixed parent are preserved; only the attempt/fence
|
|
91
|
+
* advance. This is the external-owner equivalent of retryBuild.
|
|
92
|
+
*/
|
|
93
|
+
claimExternalBuild(input: {
|
|
94
|
+
workspaceId: string;
|
|
95
|
+
rootId: RootId;
|
|
96
|
+
branchId: BranchId;
|
|
97
|
+
buildId: BuildId;
|
|
98
|
+
workerCapability?: string;
|
|
99
|
+
now?: number;
|
|
100
|
+
}): ExecutionScope;
|
|
101
|
+
failBuild(input: {
|
|
102
|
+
scope: ExecutionScope;
|
|
103
|
+
reason: string;
|
|
104
|
+
now?: number;
|
|
105
|
+
}): void;
|
|
106
|
+
cancelBuild(input: {
|
|
107
|
+
workspaceId: string;
|
|
108
|
+
rootId: RootId;
|
|
109
|
+
branchId: BranchId;
|
|
110
|
+
buildId: BuildId;
|
|
111
|
+
now?: number;
|
|
112
|
+
}): void;
|
|
113
|
+
retryBuild(input: {
|
|
114
|
+
workspaceId: string;
|
|
115
|
+
rootId: RootId;
|
|
116
|
+
branchId: BranchId;
|
|
117
|
+
buildId: BuildId;
|
|
118
|
+
}): BuildRecord;
|
|
119
|
+
/**
|
|
120
|
+
* Idempotent failure recovery for an input that was never accepted.
|
|
121
|
+
*
|
|
122
|
+
* Stop/archive may release the branch guard before the runtime reports the
|
|
123
|
+
* failure. In that case the build is still the same durable row and must be
|
|
124
|
+
* returned to QUEUED for an explicit resume instead of being stranded in
|
|
125
|
+
* RUNNING (or permanently FAILED). A committed build is never requeued.
|
|
126
|
+
*/
|
|
127
|
+
requeueBuild(input: {
|
|
128
|
+
workspaceId: string;
|
|
129
|
+
rootId: RootId;
|
|
130
|
+
branchId: BranchId;
|
|
131
|
+
buildId: BuildId;
|
|
132
|
+
}): BuildRecord;
|
|
133
|
+
forkBranch(input: {
|
|
134
|
+
commandId: string;
|
|
135
|
+
actorId: string;
|
|
136
|
+
workspaceId: string;
|
|
137
|
+
sourceBranchId: BranchId;
|
|
138
|
+
anchorBuildId: BuildId;
|
|
139
|
+
newBranchId: BranchId;
|
|
140
|
+
}): BranchRecord;
|
|
141
|
+
setPaused(input: {
|
|
142
|
+
workspaceId: string;
|
|
143
|
+
rootId: RootId;
|
|
144
|
+
branchId: BranchId;
|
|
145
|
+
paused: boolean;
|
|
146
|
+
}): boolean;
|
|
147
|
+
snapshot(input: {
|
|
148
|
+
workspaceId: string;
|
|
149
|
+
rootId: RootId;
|
|
150
|
+
branchId: BranchId;
|
|
151
|
+
}): QueueSnapshot;
|
|
152
|
+
private snapshotUnlocked;
|
|
153
|
+
events(afterCursor?: number, limit?: number): ContinuationEvent[];
|
|
154
|
+
recoverExpiredLeases(now?: number): number;
|
|
155
|
+
private terminateAttempt;
|
|
156
|
+
private releaseExpiredGuard;
|
|
157
|
+
private markDependentsBlocked;
|
|
158
|
+
private hasCommittedDescendant;
|
|
159
|
+
private isAncestorOfHead;
|
|
160
|
+
private ensureBranchRecord;
|
|
161
|
+
private requireBranch;
|
|
162
|
+
private ensureGuard;
|
|
163
|
+
/**
|
|
164
|
+
* Release the active attempt without ever moving the branch fence backward.
|
|
165
|
+
* A recovered/completed attempt's fence stays as the monotonic high-water
|
|
166
|
+
* mark; the next claim increments it again.
|
|
167
|
+
*/
|
|
168
|
+
private releaseGuard;
|
|
169
|
+
private appendEvent;
|
|
170
|
+
private mutate;
|
|
171
|
+
private acquireLock;
|
|
172
|
+
private readLedger;
|
|
173
|
+
private readLedgerUnlocked;
|
|
174
|
+
private writeLedgerUnlocked;
|
|
175
|
+
private ensureLedgerExists;
|
|
176
|
+
private invalidateCache;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=store.d.ts.map
|