<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hagicode</title>
    <description>The latest articles on DEV Community by Hagicode (@newbe36524).</description>
    <link>https://dev.to/newbe36524</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F588826%2Ff5bd5c70-a7e9-435d-b87c-43c73d4cff66.png</url>
      <title>DEV Community: Hagicode</title>
      <link>https://dev.to/newbe36524</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/newbe36524"/>
    <language>en</language>
    <item>
      <title>Integrating Reasonix 1.x with DeepSeek V4: ACP Model Selector Integration in Practice</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Fri, 19 Jun 2026 01:50:20 +0000</pubDate>
      <link>https://dev.to/newbe36524/integrating-reasonix-1x-with-deepseek-v4-acp-model-selector-integration-in-practice-19dp</link>
      <guid>https://dev.to/newbe36524/integrating-reasonix-1x-with-deepseek-v4-acp-model-selector-integration-in-practice-19dp</guid>
      <description>&lt;h1&gt;
  
  
  Integrating Reasonix 1.x with DeepSeek V4: ACP Model Selector Integration in Practice
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;This article discusses how to switch Reasonix 1.x, a local ACP CLI provider, to DeepSeek V4 in HagiCode. The focus isn't really on "getting it in," but rather on the semantic changes from Reasonix 0.x to 1.x—startup parameters were cut down to just one &lt;code&gt;-model&lt;/code&gt;, credentials and policies moved to &lt;code&gt;reasonix.toml&lt;/code&gt;. Let's walk through the pitfalls encountered and the verification path, bit by bit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Recently someone asked a pretty specific question: how to integrate Reasonix 1.x version to use DeepSeek V4 in HagiCode.&lt;/p&gt;

&lt;p&gt;At first glance it looked like a configuration question, but digging into the code revealed it's actually a CLI semantic migration problem. Reasonix is a local ACP (Agent Communication Protocol) CLI within HagiCode's multi-Agent Provider system. Its position in HagiCode's three-tier architecture is clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HagiCode.Libs&lt;/strong&gt; — &lt;code&gt;ReasonixProvider&lt;/code&gt;, &lt;code&gt;ReasonixOptions&lt;/code&gt;, wrapping &lt;code&gt;reasonix acp&lt;/code&gt; process startup, ACP handshake, streaming notification mapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hagicode-core&lt;/strong&gt; — &lt;code&gt;ReasonixCliProvider&lt;/code&gt; thin adapter, &lt;code&gt;AIProviderType.ReasonixCli = 12&lt;/code&gt;, &lt;code&gt;ReasonixGrain&lt;/code&gt;, Hero parameter mapping, health monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;web&lt;/strong&gt; — OpenAPI types, visual mapping, Hero configuration forms, multilingual copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire integration chain is already implemented in the archived proposal &lt;code&gt;openspec/changes/archive/2026-06-06-integrate-reasonix-agent-provider&lt;/code&gt;. So the question isn't "how to get Reasonix into the system" anymore, but "once it's in, how to switch the model to DeepSeek V4".&lt;/p&gt;

&lt;p&gt;The key turning point is: Reasonix 1.x and 0.x have fundamentally different ACP bootstrap semantics. This change directly determines how you configure DeepSeek V4. After all, once semantics change, even if the surface looks similar, it's a different beast entirely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll leave that hanging: to untangle this complexity of multiple providers and multiple models, HagiCode adopted a "field preservation, semantic migration" design at the Reasonix adaptation layer. I'll explain exactly why this trade-off was chosen later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project.&lt;/p&gt;

&lt;p&gt;HagiCode is an AI coding assistant project supporting multiple local/remote Agent Providers. Code is open sourced at &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Analysis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.x Cut Startup Parameters to Just One
&lt;/h3&gt;

&lt;p&gt;Look directly at &lt;code&gt;ReasonixProvider.BuildCommandArguments&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BuildCommandArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReasonixOptions&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"acp"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// Reasonix 1.x reduced ACP bootstrap to a transport-scoped provider selector.&lt;/span&gt;
    &lt;span class="nf"&gt;AppendOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;NormalizeExtraArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExtraArguments&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment is the key: 1.x condensed ACP bootstrap to a "transport-scoped provider selector." In plain English—the only flag still meaningful at startup is &lt;code&gt;-model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those legacy flags from the 0.x era are explicitly filtered out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FilteredBootstrapFlags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"-model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-dir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--dir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-effort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--effort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-budget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--budget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-transcript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--transcript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-mcp-prefix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--mcp-prefix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-yolo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--yolo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"--dangerously-skip-permissions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"--no-proxy"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unit tests directly prove this. Pass in a bunch of legacy flags, the command line comes out clean, no errors, just silently dropped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"acp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"deepseek-v4-flash"&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ReasonixOptions Fields Still There, But Semantics Changed
&lt;/h3&gt;

&lt;p&gt;Here's an interesting design choice. Fields like &lt;code&gt;Effort&lt;/code&gt;, &lt;code&gt;BudgetUsd&lt;/code&gt;, &lt;code&gt;TranscriptPath&lt;/code&gt;, &lt;code&gt;EnableYolo&lt;/code&gt;, &lt;code&gt;McpServerSpecs&lt;/code&gt;, &lt;code&gt;McpPrefix&lt;/code&gt; in &lt;code&gt;ReasonixOptions&lt;/code&gt; are all preserved, but each comment honestly states "Reasonix 1.x ACP no longer accepts ... so this value is currently ignored".&lt;/p&gt;

&lt;p&gt;This is a classic &lt;strong&gt;field preservation, semantic migration&lt;/strong&gt; pattern: caller contracts aren't broken (0.x code still compiles, still passes values), but at runtime these values are silently dropped. Policy-type things (permissions, MCP plugins, proxy) are required to move to &lt;code&gt;reasonix.toml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To put it another way, it's like your original light switch is still on the wall, but the renovation guy changed the wiring. Now the switch is just decoration, the real lighting control moved to the smart home panel. The switch looks unchanged, pressing it doesn't error out, but the light just doesn't turn on.&lt;/p&gt;

&lt;p&gt;So the core action for integrating DeepSeek V4 is actually just one sentence: &lt;strong&gt;pass the model id through the &lt;code&gt;-model&lt;/code&gt; selector, configure credentials/endpoint in &lt;code&gt;reasonix.toml&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How DeepSeek V4 Gets In
&lt;/h3&gt;

&lt;p&gt;In HagiCode's tests and README, the DeepSeek series uses the standard pattern through the &lt;code&gt;Model&lt;/code&gt; field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reasonixOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ReasonixOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;WorkingDirectory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/path/to/repo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"deepseek-flash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SessionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"reasonix-session-123"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests repeatedly show &lt;code&gt;Model = "deepseek-v4-flash"&lt;/code&gt;, corresponding to the generated command line &lt;code&gt;reasonix acp -model deepseek-v4-flash&lt;/code&gt;. The specific model id (&lt;code&gt;deepseek-v4-flash&lt;/code&gt;, &lt;code&gt;deepseek-flash&lt;/code&gt;, etc.) should follow the Reasonix 1.x version you installed and the provider aliases registered in &lt;code&gt;reasonix.toml&lt;/code&gt;—after all, whether an alias is real or not, Reasonix knows best.&lt;/p&gt;

&lt;h3&gt;
  
  
  Working Directory and Session Recovery Go Through ACP, Not CLI Flags
&lt;/h3&gt;

&lt;p&gt;This is the second semantic change in 1.x, easy to get confused about. In 0.x you used &lt;code&gt;--dir&lt;/code&gt; to specify working directory, in 1.x it changed to using &lt;code&gt;session/new&lt;/code&gt; / &lt;code&gt;session/load&lt;/code&gt; within the ACP protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sessionHandle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sessionClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartSessionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;workingDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Model selection completely determined by startup -model&lt;/span&gt;
    &lt;span class="n"&gt;startupCts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the &lt;code&gt;model&lt;/code&gt; parameter in &lt;code&gt;StartSessionAsync&lt;/code&gt; passes &lt;code&gt;null&lt;/code&gt;—model selection is completely determined by &lt;code&gt;-model&lt;/code&gt; at startup, session level no longer overrides the model. &lt;code&gt;SessionId&lt;/code&gt; is still a provider-native continuity hint, used only to resume sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Putting the above analysis together into an executable path, let's walk through four steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install Reasonix CLI
&lt;/h3&gt;

&lt;p&gt;Reasonix is a locally installed, &lt;code&gt;IsPubliclyInstallable: false&lt;/code&gt; provider, can't be installed via npm publicly. First put the &lt;code&gt;reasonix&lt;/code&gt; executable in PATH. After installing, verify with HagiCode.Libs' built-in console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run Ping scenario, execute reasonix acp handshake and report version&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; src/HagiCode.Libs.Reasonix.Console &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--test-provider&lt;/span&gt; reasonix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If handshake fails, it's usually one of two cases: either PATH didn't find &lt;code&gt;reasonix&lt;/code&gt;, or &lt;code&gt;reasonix.toml&lt;/code&gt; isn't configured. There aren't really any other reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configure DeepSeek V4 Credentials in reasonix.toml
&lt;/h3&gt;

&lt;p&gt;1.x no longer accepts startup flags like &lt;code&gt;--api-key&lt;/code&gt;, &lt;code&gt;--base-url&lt;/code&gt;. Model provider endpoint, API key, proxy policies all need to be written to &lt;code&gt;reasonix.toml&lt;/code&gt;. Configuration roughly includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DeepSeek V4 API endpoint&lt;/li&gt;
&lt;li&gt;DeepSeek API key&lt;/li&gt;
&lt;li&gt;Aliases you want to expose to the &lt;code&gt;-model&lt;/code&gt; selector (like &lt;code&gt;deepseek-v4-flash&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Specific field names should follow the documentation of your installed Reasonix version. HagiCode's side only cares about passing through &lt;code&gt;-model deepseek-v4-flash&lt;/code&gt;, how this alias resolves to the actual model is Reasonix's business—responsibility boundaries are clearly drawn, don't cross lines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure HagiCode's ProviderConfiguration
&lt;/h3&gt;

&lt;p&gt;The resolution priority in backend &lt;code&gt;ReasonixCliProvider.ResolveModel&lt;/code&gt; is: request.Model takes priority, otherwise fall back to &lt;code&gt;_config.Model&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;ResolveModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AIRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in &lt;code&gt;appsettings&lt;/code&gt; or runtime config, set the provider's Model to DeepSeek V4's alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AIProvider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ReasonixCli"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ReasonixCli"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deepseek-v4-flash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a trap that's easy to step in: &lt;code&gt;Settings&lt;/code&gt; can only hold keys within the whitelist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SupportedSettingKeys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"effort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"budgetUsd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"transcriptPath"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"enableYolo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"startupTimeoutMs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"reasoning"&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ValidateConfigurationOverrides&lt;/code&gt; will directly reject keys outside the whitelist. And most of these keys are ignored in 1.x (corresponding to those ignored fields in &lt;code&gt;ReasonixOptions&lt;/code&gt;), so &lt;strong&gt;never stuff DeepSeek credentials into Settings&lt;/strong&gt;, that's not where they belong, credentials go to &lt;code&gt;reasonix.toml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Use Console for End-to-End Verification
&lt;/h3&gt;

&lt;p&gt;After configuration, run the full suite with Reasonix's dedicated console, explicitly specifying the model as DeepSeek V4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Default suite: four scenarios - Ping / Simple Prompt / Complex Prompt / Session Resume&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; src/HagiCode.Libs.Reasonix.Console &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--test-provider-full&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; deepseek-v4-flash &lt;span class="nt"&gt;--repo&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all four scenarios pass green, the model selector, ACP handshake, streaming notifications, session recovery—the entire chain is connected. Green means peace of mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How to Fill the Hero Configuration Form in Frontend
&lt;/h3&gt;

&lt;p&gt;If you go through HagiCode's Hero career UI instead of directly modifying appsettings, after selecting Reasonix in &lt;code&gt;HeroCliEquipmentForm&lt;/code&gt;, the form fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;binary&lt;/strong&gt;: default &lt;code&gt;reasonix&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;model&lt;/strong&gt;: fill &lt;code&gt;deepseek-v4-flash&lt;/code&gt; (key field for switching to DeepSeek V4)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;effort&lt;/strong&gt;: none / low / medium / high (ignored in 1.x, but UI still keeps it)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;budgetUsd&lt;/strong&gt;: number (ignored in 1.x)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;transcriptPath&lt;/strong&gt;: text (ignored in 1.x)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;enableYolo&lt;/strong&gt;: boolean (ignored in 1.x, permissions go to toml)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;arguments&lt;/strong&gt;: extra parameters passed through to ACP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;startupTimeoutMs&lt;/strong&gt;: default 15000&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Actually, only the &lt;code&gt;model&lt;/code&gt; field affects DeepSeek V4 behavior, the rest are just decoration under 1.x. This is also HagiCode's "field preservation, semantic migration" design reflected in the UI—the form doesn't break old user habits, but the fields that actually take effect are converged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session Binding and Recovery
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ReasonixCliProvider&lt;/code&gt; uses &lt;code&gt;ConcurrentDictionary&amp;lt;string, string&amp;gt;&lt;/code&gt; to maintain session bindings, binding key is calculated from sessionId, working directory, executable path, and model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bindingKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NormalizedAcpCliAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildBindingKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;effectiveRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecutablePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means if you switch models mid-session in the same session, the binding key changes and it's treated as a new session. So &lt;strong&gt;after integrating DeepSeek V4, keep the model alias stable throughout the session lifecycle&lt;/strong&gt;, otherwise resume will break. I learned this the hard way through actual testing—blood and tears lesson, still remember the taste.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring and Degradation
&lt;/h3&gt;

&lt;p&gt;Reasonix uses the &lt;code&gt;Provider&lt;/code&gt; strategy (not &lt;code&gt;Grain&lt;/code&gt; strategy) in &lt;code&gt;AgentCliMonitoringRegistry&lt;/code&gt;, since it might not be installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;AgentCliMonitoringDescriptor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;CliId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"reasonix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DisplayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Reasonix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProviderType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ping-based, via PATH discovery&lt;/span&gt;
    &lt;span class="n"&gt;ExecutableCandidates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"reasonix"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frontend health checks show whether Reasonix is available. If &lt;code&gt;reasonix&lt;/code&gt; isn't in PATH, the UI gracefully degrades to "unavailable"—this logic is built in, no need to worry about it yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Several Practical Points to Note
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authenticity of Model Aliases&lt;/strong&gt;: &lt;code&gt;deepseek-v4-flash&lt;/code&gt; must be a real alias registered in &lt;code&gt;reasonix.toml&lt;/code&gt;, otherwise ACP handshake passes but sending prompts still fails. Verify with console first before going to Hero, don't cut corners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't Use &lt;code&gt;arguments&lt;/code&gt; to Pass Legacy Flags&lt;/strong&gt;: &lt;code&gt;NormalizeExtraArguments&lt;/code&gt; filters out &lt;code&gt;--effort&lt;/code&gt;, &lt;code&gt;--budget&lt;/code&gt;, etc., passing them is futile, wasted effort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials Only in toml&lt;/strong&gt;: API key, endpoint, proxy, MCP plugins all go in &lt;code&gt;reasonix.toml&lt;/code&gt;, HagiCode's side Settings whitelist doesn't have these fields at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;startupTimeoutMs is Adjustable&lt;/strong&gt;: If DeepSeek V4 cold start is slow, raise &lt;code&gt;startupTimeoutMs&lt;/code&gt; from default 15000, this field is recognized in 1.x.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Economic System Maps to Claude Bucket&lt;/strong&gt;: Frontend &lt;code&gt;resolveEconomicSystemByExecutorType&lt;/code&gt; maps Reasonix to the &lt;code&gt;'claude'&lt;/code&gt; bucket, purely for display, doesn't affect billing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A Minimal Verification Path
&lt;/h3&gt;

&lt;p&gt;If you just want to confirm DeepSeek V4 works quickly without touching Hero UI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install reasonix, configure &lt;code&gt;reasonix.toml&lt;/code&gt; (DeepSeek endpoint + key + alias)&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;ReasonixCli.Model = "deepseek-v4-flash"&lt;/code&gt; in &lt;code&gt;appsettings&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;dotnet run --project src/HagiCode.Libs.Reasonix.Console -- --test-provider-full --model deepseek-v4-flash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All four scenarios green, integration complete&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Returning to the original question—"how to integrate Reasonix 1.x to use DeepSeek v4".&lt;/p&gt;

&lt;p&gt;The answer is actually just one sentence: &lt;strong&gt;pass the model alias through the &lt;code&gt;-model&lt;/code&gt; selector, configure credentials and policies in &lt;code&gt;reasonix.toml&lt;/code&gt;, don't count on CLI flags&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Behind that one sentence is a pretty clean semantic convergence by Reasonix 1.x: startup parameters cut to just &lt;code&gt;-model&lt;/code&gt;, working directory and session recovery moved into ACP protocol, policies all sunk into toml. HagiCode's adaptation layer didn't fight this change head-on, but chose the gentle "field preservation, semantic migration" route—old code still compiles, still passes values, silently ignored at runtime, converging effective switches to just &lt;code&gt;-model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The benefit of this trade-off is smooth migration, the cost is documentation needs to be clear—which is why this article exists. Just remember three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Models go through &lt;code&gt;-model&lt;/code&gt;&lt;/strong&gt;, DeepSeek V4 is &lt;code&gt;-model deepseek-v4-flash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials go to toml&lt;/strong&gt;, don't stuff them into Settings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't switch models mid-session&lt;/strong&gt;, binding key changes, resume breaks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;HagiCode chose to design the Reasonix adaptation layer this way essentially because it needs to accommodate multiple providers, multiple model versions, multiple deployment forms. This complexity of multiple languages, multiple platforms is exactly why we repeatedly polish the provider adaptation strategy in HagiCode.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reasonix Provider implementation: &lt;code&gt;repos/Hagicode.Libs/src/HagiCode.Libs.Providers/Reasonix/ReasonixProvider.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reasonix Options field semantics: &lt;code&gt;repos/Hagicode.Libs/src/HagiCode.Libs.Providers/Reasonix/ReasonixOptions.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backend thin adapter: &lt;code&gt;repos/hagicode-core/src/PCode.ClaudeHelper/AI/Providers/ReasonixCliProvider.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Integration proposal archive: &lt;code&gt;openspec/changes/archive/2026-06-06-integrate-reasonix-agent-provider&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backend spec: &lt;code&gt;openspec/specs/reasonix-backend-integration/spec.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Unit tests (including deepseek-v4-flash cases): &lt;code&gt;repos/Hagicode.Libs/tests/HagiCode.Libs.Providers.Tests/ReasonixProviderTests.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;HagiCode official site: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Around "Integrating Reasonix 1.x with DeepSeek V4: ACP Model Selector Integration in Practice," a more solid approach is to first gradually get key configurations, dependency boundaries, and implementation paths working, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;When goals, steps, and acceptance criteria are clear, this type of solution usually flows more smoothly into actual delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-19-reasonix-1x-deepseek-v4-acp-integration%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-19-reasonix-1x-deepseek-v4-acp-integration%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reasonix</category>
      <category>deepseek</category>
      <category>acp</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>Pi Agent Integration: Message Parsing, Retry, and Cancellation</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Fri, 19 Jun 2026 01:12:00 +0000</pubDate>
      <link>https://dev.to/newbe36524/pi-agent-integration-message-parsing-retry-and-cancellation-nl0</link>
      <guid>https://dev.to/newbe36524/pi-agent-integration-message-parsing-retry-and-cancellation-nl0</guid>
      <description>&lt;h1&gt;
  
  
  Pi Agent Integration: Message Parsing, Retry, and Cancellation
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;When integrating a CLI-based AI agent, you can't avoid three things: how to translate its private event stream into stable messages, who's responsible for retry after failures, and how to cleanly stop the process when users click cancel. These three things essentially boil down to "clarifying responsibilities"—it's simple in theory, but you only realize how deep the water is when you actually do it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Recently, I've been working on an AI coding assistant project, and one of the agents to integrate is &lt;a href="https://github.com/earendil-works/pi-coding-agent" rel="noopener noreferrer"&gt;pi&lt;/a&gt;. It's a TUI/CLI coding agent that outputs JSON events line by line to stdout when running. Sounds simple—spawn the process, read output, parse it—but when you actually start, you realize "integrating an agent CLI" is completely different from "integrating a regular CLI."&lt;/p&gt;

&lt;p&gt;With a regular CLI, you read stdout, get an exit code, and that's it. But agent CLIs have three particularly headache-inducing characteristics:&lt;/p&gt;

&lt;p&gt;First, its event stream is a &lt;strong&gt;private protocol&lt;/strong&gt;. &lt;code&gt;turn_start&lt;/code&gt;, &lt;code&gt;session&lt;/code&gt;, &lt;code&gt;message_update&lt;/code&gt;, &lt;code&gt;message_end&lt;/code&gt;, &lt;code&gt;turn_end&lt;/code&gt;, &lt;code&gt;agent_end&lt;/code&gt;—these are defined by pi itself, not any industry standard. Every upper layer that wants to consume it has to handle it separately, effectively leaking pi's internal details everywhere. It's like looking at someone from a distance—you think you see them clearly, but you're only seeing the side they want to show you.&lt;/p&gt;

&lt;p&gt;Second, its failure semantics are &lt;strong&gt;particularly ambiguous&lt;/strong&gt;. The agent might encounter network jitter, model rate limiting, or process crashes while running. Should it retry? Where to retry? Will retry mess up the session state that's already half-output? This is an architectural decision, not something you can solve by casually writing a &lt;code&gt;for&lt;/code&gt; loop.&lt;/p&gt;

&lt;p&gt;Third, it's &lt;strong&gt;long-running and interruptible&lt;/strong&gt;. A single turn might run for tens of seconds or even minutes, and users might want to cancel at any time. When canceled, the process can't become an orphan, tool calls can't be left half-finished, and already output content can't be lost. The water here is much deeper than imagined.&lt;/p&gt;

&lt;p&gt;To solve these pain points, we spent some time straightening out the integration path. I'll get into specifics later, but here's a spoiler: the real difficulty isn't in "spawning the process," but in "clarifying responsibilities."&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The solution shared in this article comes from the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project—an AI coding assistant that supports multiple models and multiple agent CLI backends. GitHub repository: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site&lt;/a&gt;, feel free to star it. All the code and all the pitfalls mentioned below are actually running in this project. Writing this out is just leaving myself a memory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overall Layering
&lt;/h2&gt;

&lt;p&gt;HagiCode splits AI capability integration into two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The bottom layer is &lt;code&gt;Hagicode.Libs&lt;/code&gt;, providing reusable provider primitives &lt;code&gt;ICliProvider&amp;lt;TOptions&amp;gt;&lt;/code&gt;, specifically responsible for "spawning a CLI agent and normalizing its output into a shared message stream."&lt;/li&gt;
&lt;li&gt;The upper layer is &lt;code&gt;hagicode-core&lt;/code&gt;, providing project-level thin adapters &lt;code&gt;IAIProvider&lt;/code&gt;, responsible for "translating business requests into provider parameters, consuming shared message streams, and exposing unified streaming chunks externally."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pi's integration follows this path. The bottom layer &lt;code&gt;PiProvider&lt;/code&gt; spawns the pi process, reads the JSON event stream, and normalizes it into shared messages; the upper layer &lt;code&gt;PiCliProvider&lt;/code&gt; translates &lt;code&gt;AIRequest&lt;/code&gt; into &lt;code&gt;PiOptions&lt;/code&gt;, consumes &lt;code&gt;CliMessage&lt;/code&gt;, and outputs &lt;code&gt;AIStreamingChunk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These three things—message parsing, retry, cancellation—fall into three different places: &lt;code&gt;PiJsonEventMapper&lt;/code&gt;, a seemingly strange archiving proposal, and &lt;code&gt;CliProcessManager&lt;/code&gt;. Let's talk about each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Message Parsing: Converting Pi's Private Events to Shared Messages
&lt;/h2&gt;

&lt;p&gt;pi outputs JSON events line by line under &lt;code&gt;--mode json --print&lt;/code&gt;. This event set is private to pi and must not be leaked directly to upper layers, otherwise every consumer would couple to pi's internal details, and the entire project would follow pi's upgrades when its event structure changes. This kind of leakage is similar to writing your thoughts on your face—others find it tiring to look at, and you're not necessarily comfortable either.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;PiJsonEventMapper&lt;/code&gt; as a translation layer to normalize pi's events into shared &lt;code&gt;CliMessage&lt;/code&gt;. &lt;code&gt;CliMessage&lt;/code&gt; is defined in &lt;code&gt;HagiCode.Libs.Core/Transport/CliMessage.cs&lt;/code&gt; with a very simple structure—just a &lt;code&gt;(Type, Content)&lt;/code&gt; record. The mapping relationship is roughly as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pi event&lt;/th&gt;
&lt;th&gt;shared message&lt;/th&gt;
&lt;th&gt;purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;session.started&lt;/code&gt; / &lt;code&gt;session.resumed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;session lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;message_update&lt;/code&gt; (text type)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;assistant&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;streaming body incremental&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;message_update&lt;/code&gt; (thinking type)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;assistant.thought&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;thought chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;message_update&lt;/code&gt; (tool type)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tool.call&lt;/code&gt; / &lt;code&gt;tool.update&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;tool call initiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;message_end&lt;/code&gt; / &lt;code&gt;turn_end&lt;/code&gt; (toolResult)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tool.completed&lt;/code&gt; / &lt;code&gt;tool.failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;tool result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;turn_end&lt;/code&gt; / &lt;code&gt;agent_end&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terminal.completed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;current turn end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;non-zero exit / parse failure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terminal.failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;terminal state failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is just a quick reference, but it contains two key techniques that were figured out after stumbling into pitfalls—worth expanding on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technique One: Converting Cumulative Snapshot to Delta
&lt;/h3&gt;

&lt;p&gt;This is the easiest place to crash. pi's &lt;code&gt;message_update&lt;/code&gt; event doesn't send increments, but &lt;strong&gt;cumulative full text&lt;/strong&gt;—every time a token comes, it resends the "complete text so far."&lt;/p&gt;

&lt;p&gt;If you forward received content directly to the frontend, users will see content repeated: the first is "you", the second is "hello", the third is "hello,", the fourth is "hello, wor"... The frontend will think these are four independent outputs. Repetition is fresh the first time you see it, but boring the tenth time.&lt;/p&gt;

&lt;p&gt;The solution is prefix comparison to calculate the true delta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Key: pi sends cumulative snapshot, not increment&lt;/span&gt;
&lt;span class="c1"&gt;// Use prefix comparison to extract the delta, otherwise frontend sees repeated content&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_lastAssistantTextSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_lastAssistantTextSnapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;..];&lt;/span&gt;
    &lt;span class="n"&gt;_lastAssistantTextSnapshot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's another hidden pitfall here: &lt;strong&gt;cross-turn prefix replay&lt;/strong&gt;. After tool calls end and the assistant continues speaking, pi will replay that previous text from the beginning again. If you only keep a global snapshot, you'll treat the replayed content as increments, causing repetition after tool calls. &lt;code&gt;PiProviderTests&lt;/code&gt; has a dedicated test case &lt;code&gt;ExecuteAsync_deduplicates_replayed_assistant_prefix_after_tool_turns&lt;/code&gt; covering this scenario. In other words, snapshots before and after tool calls need aligned processing, not independent handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technique Two: Buffer Thinking Until Turn End
&lt;/h3&gt;

&lt;p&gt;Thought chains (thinking) can't be output as soon as each token is received. pi stuffs a bunch of thinking fragments in the middle of tool calls. If forwarded in real time, the stream order becomes a mess—一会儿是 assistant 正文，一会儿是思考碎片，一会儿又是 tool.call。Does this make sense? It doesn't really, just adding confusion.&lt;/p&gt;

&lt;p&gt;Our approach: when receiving thinking events, first put them in &lt;code&gt;BufferThinkingSnapshot&lt;/code&gt; for temporary storage, and wait until &lt;code&gt;message_end&lt;/code&gt; or &lt;code&gt;turn_end&lt;/code&gt; with &lt;code&gt;stopReason != "toolUse"&lt;/code&gt;, then uniformly &lt;code&gt;DrainBufferedThinkingMessages&lt;/code&gt;. This way, thinking fragments in the middle of tool calls won't pollute the main stream, and the complete thought process is given all at once when the turn ends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fault Tolerance: Bad Lines Can't Crash the Stream
&lt;/h3&gt;

&lt;p&gt;Agent CLI isn't the ideal system from textbooks. It occasionally spits out a non-JSON line, or a JSON without a &lt;code&gt;type&lt;/code&gt; field. If you throw an exception here, the entire stream dies and users see nothing. The real world isn't always perfect—who can guarantee every line is well-behaved?&lt;/p&gt;

&lt;p&gt;Our strategy: any line that fails to parse doesn't interrupt the stream, but is collected into &lt;code&gt;_invalidOutputLines&lt;/code&gt;. After the process ends, in &lt;code&gt;Complete()&lt;/code&gt;, these "bad lines" are spliced into the diagnostic text of &lt;code&gt;terminal.failed&lt;/code&gt;. This way, when users see errors, they can directly see what garbage pi actually output, not a dry "parse error."&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry: If Provider Layer Doesn't Do It, Who Does?
&lt;/h2&gt;

&lt;p&gt;This is the easiest pitfall in the entire integration. Intuitively "integrating a CLI should include retry," but HagiCode in an archiving proposal &lt;strong&gt;actively removed&lt;/strong&gt; all automatic retry from the provider layer. The proposal is called &lt;code&gt;remove-provider-auto-retry-support&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why No Automatic Retry
&lt;/h3&gt;

&lt;p&gt;The proposal background is written very bluntly. Retry logic was originally scattered in two places: one copy in &lt;code&gt;Hagicode.Libs&lt;/code&gt; (OpenCode-style fresh-runtime replay), another in &lt;code&gt;hagicode-core&lt;/code&gt; (&lt;code&gt;ProviderErrorAutoRetryCoordinator&lt;/code&gt;). Both sides did their own thing, making "whether to retry" a hidden implicit behavior inside the provider, secretly changing failure timing, session continuation method, and chat state flow.&lt;/p&gt;

&lt;p&gt;Think about it and your head hurts: user sends a message, provider internally retries three times itself, first two fail, third succeeds. Upper layer has no idea what happened in the middle, session state, token counting, UI progress all don't match. This kind of implicit behavior is chronic poison in architecture.&lt;/p&gt;

&lt;p&gt;So the boundary was converged into one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;provider converges to single-attempt semantics, caller needs to treat non-retry state as normal single-execution result.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What Does This Mean for PiProvider
&lt;/h3&gt;

&lt;p&gt;In code, it's three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PiOptions&lt;/code&gt; has &lt;strong&gt;no retry-related fields&lt;/strong&gt;—no &lt;code&gt;maxAttempts&lt;/code&gt;, no &lt;code&gt;retryDelay&lt;/code&gt;, no &lt;code&gt;retryClassifier&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecuteAsync&lt;/code&gt; ends after one pi process run completes, failures directly give &lt;code&gt;terminal.failed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Classifiers previously serving automatic retry (&lt;code&gt;ClaudeCodeRetryableTerminalFailureClassifier&lt;/code&gt;, &lt;code&gt;CodexRetryableTerminalFailureClassifier&lt;/code&gt;, etc.)—if they purely served automatic retry—are all removed from the active path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But please note, &lt;strong&gt;retry capability hasn't disappeared, just moved up&lt;/strong&gt;. The proposal explicitly writes "leaving a stable boundary for higher layers to uniformly take over retry later." The DTO for &lt;code&gt;providerErrorAutoRetry&lt;/code&gt; configuration item, normalization, serialization, frontend settings page round-trip are all preserved, it just no longer drives provider execution. Some things aren't really unwanted, just kept in a different way.&lt;/p&gt;

&lt;h3&gt;
  
  
  What If You Want to Retry
&lt;/h3&gt;

&lt;p&gt;If you want to add retry on top of pi, the correct approach is to do it at the caller of &lt;code&gt;PiCliProvider&lt;/code&gt;—for example, your session orchestration layer (in HagiCode it's Orleans's &lt;code&gt;SessionGrain&lt;/code&gt;, frontend might be chat orchestration layer). After getting &lt;code&gt;terminal.failed&lt;/code&gt;, judge yourself whether it's retryable, decide delay and count yourself, then send &lt;code&gt;ExecuteAsync&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;A minimal viable pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Retry logic at caller, don't stuff back into PiProvider&lt;/span&gt;
&lt;span class="c1"&gt;// Otherwise breaks the "single-attempt" boundary just established by provider&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AIResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExecuteWithRetryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AIRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Return on success or reaching limit&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FinishReason&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;FinishReason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unknown&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Only retry retryable terminal failures (network, 5xx, process crash)&lt;/span&gt;
        &lt;span class="c1"&gt;// model rejected, auth failure这类重试也无意义，别重试&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classification logic for judging "retryable" is no longer in the provider, caller defines it themselves. &lt;code&gt;providerErrorAutoRetry&lt;/code&gt; configuration (maxAttempts, retryDelay, enabled) can still be read from the frontend settings page, but what actually drives retry is your orchestration layer, not &lt;code&gt;PiProvider&lt;/code&gt;. Repeat this three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancellation: Token Pass-through + Three-stage Shutdown
&lt;/h2&gt;

&lt;p&gt;For cancellation, PiProvider almost doesn't implement anything itself, fully delegating to &lt;code&gt;CliProcessManager&lt;/code&gt;. PiProvider only handles two things: passing &lt;code&gt;CancellationToken&lt;/code&gt; down, and cleanup on exception.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full-chain Pass-through
&lt;/h3&gt;

&lt;p&gt;The chain looks like this, passing all the way down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;调用方 CancellationToken
  → PiCliProvider.StreamCoreAsync(cancellationToken)
  → PiProvider.ExecuteProcessAsync([EnumeratorCancellation] cancellationToken)
  → ReadLineAsync(cancellationToken) / WaitForExitAsync(cancellationToken)
  → 异常时 _processManager.StopAsync(handle, CancellationToken.None)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the last line: cleanup uses &lt;code&gt;CancellationToken.None&lt;/code&gt;, not the token the user passed in. This is a detail, but extremely important.&lt;/p&gt;

&lt;p&gt;The reason is: the user's token &lt;strong&gt;is already canceled&lt;/strong&gt;. If you use this already-canceled token for cleanup, the cleanup task will be canceled immediately, and the process becomes an orphan—pi still running in the background, no one collecting it, CPU and memory occupied for nothing. So cleanup must use &lt;code&gt;CancellationToken.None&lt;/code&gt;, ensuring cleanup actions definitely complete. It's like with people—some things need proper cleanup after they completely stop, otherwise it's just leaving a mess.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three-stage Progressive Shutdown
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CliProcessManager.StopProcessAsync&lt;/code&gt; is a three-stage progressive shutdown process, with time constants defined at the top of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 优雅停止的耐心：先给进程自己收尾的时间&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;GracefulStopTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 强制 kill 后等待进程真正退出的耐心&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;StopWaitTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three stages progress like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Interrupt signal&lt;/strong&gt;. &lt;code&gt;TryInterruptAsync&lt;/code&gt; first writes a &lt;code&gt;\u0003&lt;/code&gt; (Ctrl+C character) to stdin, and on Unix additionally does &lt;code&gt;kill -INT &amp;lt;pid&amp;gt;&lt;/code&gt;. This step is to let pi gracefully end itself—it can perceive the interrupt and wrap up what it's writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful wait&lt;/strong&gt;. Wait at most 2 seconds to see if the process exits on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force kill&lt;/strong&gt;. If it hasn't exited, directly &lt;code&gt;Process.Kill(entireProcessTree: true)&lt;/code&gt; to kill the entire process tree together, then wait at most 5 seconds to confirm it's really dead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why &lt;code&gt;entireProcessTree: true&lt;/code&gt;? Because pi spawns child processes when running tools—for example, local model processes routed by provider, bash subprocesses running. Killing only the parent process leaves child processes as orphans continuing to run. Killing the whole tree together is clean.&lt;/p&gt;

&lt;p&gt;Under Windows there's no SIGINT, can only rely on Ctrl+C character, so cross-platform behavior will differ, keep this in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  PiProvider's Exception Cleanup
&lt;/h3&gt;

&lt;p&gt;PiProvider's &lt;code&gt;ExecuteProcessAsync&lt;/code&gt; when &lt;code&gt;ReadLineAsync&lt;/code&gt; throws an exception, uses &lt;code&gt;ExceptionDispatchInfo.Capture&lt;/code&gt; to temporarily store the exception, jumps out of the loop to call &lt;code&gt;StopAsync&lt;/code&gt; to clean up the process, then &lt;code&gt;pendingException.Throw()&lt;/code&gt; rethrows the original exception to the upper layer.&lt;/p&gt;

&lt;p&gt;Why store then throw? Because if you throw directly, the process hasn't been recycled yet and becomes an orphan; if you throw before &lt;code&gt;StopAsync&lt;/code&gt;, cleanup logic doesn't run at all. Store it temporarily, first guarantee the process is definitely recycled, then preserve the original &lt;code&gt;OperationCanceledException&lt;/code&gt; semantics completely for the caller—caller gets this exception and can judge "oh, user actively canceled" not "something went wrong."&lt;/p&gt;

&lt;h2&gt;
  
  
  Unified Contract for Startup Failures
&lt;/h2&gt;

&lt;p&gt;There's another detail worth mentioning separately. Process startup failure—for example, pi executable doesn't exist, wrong permissions—PiProvider &lt;strong&gt;doesn't throw exceptions&lt;/strong&gt;, but synthesizes a &lt;code&gt;terminal.failed&lt;/code&gt; message, then &lt;code&gt;yield break&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why do this? Because if you throw exceptions, upper layer consumers have to handle two completely different semantics: one is "normal messages during streaming consumption," the other is "exceptions thrown before streaming starts." This makes the consumer's &lt;code&gt;await foreach&lt;/code&gt; particularly hard to write.&lt;/p&gt;

&lt;p&gt;After unifying to "always give you messages first, then end the stream," consumer logic is consistent: getting &lt;code&gt;terminal.failed&lt;/code&gt; counts as failure, getting &lt;code&gt;terminal.completed&lt;/code&gt; counts as success, no need for try/catch branching. This is a small but important design decision, stabilizing the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice: Correct Way to Consume Streams
&lt;/h2&gt;

&lt;p&gt;Referencing &lt;code&gt;PiScenarioMessageReader&lt;/code&gt; (libs console test scenario) and &lt;code&gt;PiCliProvider.StreamCoreAsync&lt;/code&gt; (core thin adapter) in HagiCode, consumers look roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. 短路处理失败，别再处理后续消息&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NormalizedAcpCliAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetFailureMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;AIStreamingChunk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StreamingChunkType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// stream ends after terminal.failed&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. assistant 文本是 cumulative snapshot，自己再做一次增量计算&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"assistant"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;TryGetText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ReconcileSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 前缀比对&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. terminal.completed 是唯一可靠的"结束"信号&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"terminal.completed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Common Pitfalls Quick Reference
&lt;/h3&gt;

&lt;p&gt;Organizing all the pitfalls encountered along the way into a table, for future reference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;前端看到 assistant 文本重复&lt;/td&gt;
&lt;td&gt;没做 cumulative 转 delta&lt;/td&gt;
&lt;td&gt;用 &lt;code&gt;ReconcileAssistantTextSnapshot&lt;/code&gt; 做前缀比对&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;取消后进程还在跑&lt;/td&gt;
&lt;td&gt;清理用了已经取消的 token&lt;/td&gt;
&lt;td&gt;改用 &lt;code&gt;CancellationToken.None&lt;/code&gt; 做清理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重试不生效&lt;/td&gt;
&lt;td&gt;把重试写进了 PiProvider，但 provider 是单次尝试语义&lt;/td&gt;
&lt;td&gt;上移到调用方编排层&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pi 报错信息丢失&lt;/td&gt;
&lt;td&gt;没读 &lt;code&gt;terminal.failed&lt;/code&gt; 的诊断字段&lt;/td&gt;
&lt;td&gt;完整透传 &lt;code&gt;text&lt;/code&gt; / &lt;code&gt;invalid_output_lines&lt;/code&gt; / &lt;code&gt;stderr&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工具调用中途收到思考碎片&lt;/td&gt;
&lt;td&gt;直接转发了 thinking 事件&lt;/td&gt;
&lt;td&gt;缓冲到 turn 结束再 &lt;code&gt;DrainBufferedThinkingMessages&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How to Verify
&lt;/h3&gt;

&lt;p&gt;The libs layer uses &lt;code&gt;StubCliProcessManager&lt;/code&gt; to mock processes, with unit tests covering pure logic like parameter construction, event normalization, incremental deduplication, failure pass-through. The real CLI path uses &lt;code&gt;HAGICODE_REAL_CLI_TESTS&lt;/code&gt; environment variable to opt-in, running trip scenarios with real models. The core layer's &lt;code&gt;PiCliProviderTests&lt;/code&gt; verifies the thin adapter's &lt;code&gt;AIStreamingChunk&lt;/code&gt; projection and session binding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 在 Hagicode.Libs 仓库跑 Pi 相关单测&lt;/span&gt;
dotnet &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"FullyQualifiedName~PiProviderTests"&lt;/span&gt;

&lt;span class="c"&gt;# 跑真实 CLI 集成测试（需要本地装好 pi）&lt;/span&gt;
&lt;span class="nv"&gt;HAGICODE_REAL_CLI_TESTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 dotnet &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"FullyQualifiedName~PiProviderTests.RealCli"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Putting these three things together, the mental model for integrating pi actually boils down to one sentence: &lt;strong&gt;let each layer do only its own thing.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Message parsing is handed to &lt;code&gt;PiJsonEventMapper&lt;/code&gt;: private events are normalized into shared &lt;code&gt;CliMessage&lt;/code&gt;, cumulative snapshots converted to deltas, thinking buffered until turn end.&lt;/li&gt;
&lt;li&gt;Retry is handed to the caller: provider does single attempts, whoever wants retry does it at the upper layer themselves, configuration is preserved but no longer drives provider.&lt;/li&gt;
&lt;li&gt;Cancellation is handed to &lt;code&gt;CliProcessManager&lt;/code&gt;: &lt;code&gt;CancellationToken&lt;/code&gt; is passed through the full chain, cleanup uses &lt;code&gt;CancellationToken.None&lt;/code&gt;, three-stage progressive shutdown (interrupt signal → graceful wait → force kill entire process tree).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After these boundaries are clearly drawn, integrating a new agent CLI almost becomes pipeline work—you just need to write a new &lt;code&gt;XxxProvider&lt;/code&gt; and &lt;code&gt;XxxJsonEventMapper&lt;/code&gt;, and cross-cutting logic like retry, cancellation, message contracts, error handling are all reused. This is also the fundamental reason why HagiCode can simultaneously support multiple agent CLI backends (claude code, codex, pi, gemini cli, etc.) without getting messy.&lt;/p&gt;

&lt;p&gt;Let me say that most important boundary one more time: &lt;strong&gt;don't add retry at the provider layer&lt;/strong&gt;. Once you understand this, integrating agent CLI is more than halfway done...&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Returning to the theme "Pi Agent Integration: Message Parsing, Retry, and Cancellation," what's really worth repeatedly confirming isn't scattered techniques, but whether constraints, implementation boundaries, and engineering trade-offs have been clearly seen.&lt;/p&gt;

&lt;p&gt;As long as the judgment bases in this article are settled into stable checklist items, you can make reliable decisions faster when facing similar problems in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-19-pi-agent-integration-message-parsing-retry-cancel%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-19-pi-agent-integration-message-parsing-retry-cancel%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hagicode</category>
      <category>pi</category>
      <category>cli</category>
      <category>provider</category>
    </item>
    <item>
      <title>How to Use Upptime to Build Your Own Status Page for Free</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Thu, 18 Jun 2026 05:30:22 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-to-use-upptime-to-build-your-own-status-page-for-free-2ipc</link>
      <guid>https://dev.to/newbe36524/how-to-use-upptime-to-build-your-own-status-page-for-free-2ipc</guid>
      <description>&lt;h1&gt;
  
  
  How to Use Upptime to Build Your Own Status Page for Free
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Move monitoring entirely into a GitHub repo—Actions as probes, the repo as a database, Pages as a CDN, and Issues as an event log. Zero servers, zero monthly fees, yet somehow you end up with a status page that you can view, query, and trace. Call it black magic or the wisdom of the frugal—either way, it runs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;When operating a small product matrix consisting of over a dozen external services, "is it actually working" became a common refrain. Customers report they can't access it, you SSH in and &lt;code&gt;curl&lt;/code&gt; once to find it's fine; a few minutes later it goes down, but this time you weren't watching. Commercial monitoring (Pingdom, UptimeRobot's premium tier, Datadog) can certainly solve this, but they charge either by site or by request count—for an indie developer, neither cost nor mental burden is quite worth it.&lt;/p&gt;

&lt;p&gt;More importantly, users need to be able to check the status page themselves. Ideally, one domain (say &lt;code&gt;status.hagicode.com&lt;/code&gt;) would display real-time availability for each service, response time curves, historical events, and automatically log incidents when failures occur with automatic notifications. The traditional approach requires four pieces—a server running cron, a database storing historical data, a frontend site, and a CDN. Once you lay out these four, operational costs immediately outweigh the services being monitored—using a sledgehammer to crack a nut, and even the nut feels crowded.&lt;/p&gt;

&lt;p&gt;To address these pain points, we made a decision: move the entire monitoring solution directly to GitHub. This decision brought changes larger than you might imagine—I'll get to that gradually.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our experience building &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt;. HagiCode is an AI code assistant project that exposes over a dozen public services including web pages, documentation sites, and download endpoints, driven by the main repository &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site&lt;/a&gt;. These sites must remain stable and available, so status monitoring is not optional for us—it's a necessity. The Upptime solution below is exactly what HagiCode's production environment uses—I didn't make this up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis: How Upptime Actually Works
&lt;/h2&gt;

&lt;p&gt;Upptime is essentially a GitHub repository template plus six workflows generated by that template. The key to understanding it is seeing clearly "who calls whom, when, and produces what that lands where." Break it apart and it's not so mysterious.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Flow: One Configuration File Drives Everything
&lt;/h3&gt;

&lt;p&gt;The entire system revolves around a single declarative configuration file &lt;code&gt;.upptimerc.yml&lt;/code&gt;. HagiCode's actual configuration structure looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HagiCode-org&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upptime&lt;/span&gt;

&lt;span class="na"&gt;sites&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HagiCode Website&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hagicode.com&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HagiCode Docs&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://docs.hagicode.com&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Server Package Index&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://index.hagicode.com/server/index.json&lt;/span&gt;
  &lt;span class="c1"&gt;# ... 14 sites total&lt;/span&gt;

&lt;span class="na"&gt;status-website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status.hagicode.com&lt;/span&gt;
  &lt;span class="na"&gt;logoUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://raw.githubusercontent.com/HagiCode-org/upptime/master/assets/upptime-icon.svg&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HagiCode Status&lt;/span&gt;
  &lt;span class="na"&gt;introTitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**HagiCode&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Status**"&lt;/span&gt;
  &lt;span class="na"&gt;introMessage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Real-time availability tracking for public HagiCode websites and download endpoints.&lt;/span&gt;
  &lt;span class="na"&gt;navbar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Status&lt;/span&gt;
      &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub&lt;/span&gt;
      &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/$OWNER/$REPO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two points here are worth calling out. First, &lt;code&gt;sites&lt;/code&gt; can monitor both web pages (returning HTML) and pure JSON endpoints (like &lt;code&gt;index.json&lt;/code&gt;)—Upptime only cares about HTTP status codes and response time, not content validation. Second, &lt;code&gt;cname&lt;/code&gt; points to &lt;code&gt;status.hagicode.com&lt;/code&gt;, which requires you to own that domain and configure DNS to point to GitHub Pages—after all, freebies aside, you still need to provide your own domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Division of Six Workflows
&lt;/h3&gt;

&lt;p&gt;All files under &lt;code&gt;.github/workflows/&lt;/code&gt; have a warning at the top &lt;code&gt;Do not edit this file directly!&lt;/code&gt;—they're auto-updated weekly by the template; you only need to modify &lt;code&gt;.upptimerc.yml&lt;/code&gt;. Each workflow is triggered by cron, calling different subcommands of the same action &lt;code&gt;upptime/uptime-monitor@v1.42.6&lt;/code&gt; with clear division of labor, which is actually quite reassuring:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;cron&lt;/th&gt;
&lt;th&gt;command&lt;/th&gt;
&lt;th&gt;purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uptime.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*/5 * * * *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;probe every 5 minutes, write &lt;code&gt;history/*.yml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;response-time.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;response-time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;calculate response time statistics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;graphs.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;graphs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;generate day/week/month/year PNG curves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;summary.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;summary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;update status table in README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;site.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 1 * * *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;site&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;build static site daily, deploy to Pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update-template.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 * * *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;sync upstream template weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The core snippet of &lt;code&gt;uptime.yml&lt;/code&gt; shows how the "probe" runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*/5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_PAT || github.token }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check endpoint status&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upptime/uptime-monitor@v1.42.6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GH_PAT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_PAT || github.token }}&lt;/span&gt;
          &lt;span class="na"&gt;SECRETS_CONTEXT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ toJson(secrets) }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;site.yml&lt;/code&gt; adds one more step, using &lt;code&gt;peaceiris/actions-gh-pages@v4&lt;/code&gt; to push build artifacts to the &lt;code&gt;gh-pages&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;peaceiris/actions-gh-pages@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;github_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_PAT || github.token }}&lt;/span&gt;
    &lt;span class="na"&gt;publish_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;site/status-page/__sapper__/export/"&lt;/span&gt;
    &lt;span class="na"&gt;user_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Upptime&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Bot"&lt;/span&gt;
    &lt;span class="na"&gt;user_email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;73812536+upptime-bot@users.noreply.github.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Data Persistence: Files as Database
&lt;/h3&gt;

&lt;p&gt;Monitoring results aren't stored in a database; instead they're committed back to the repository as files. This sounds a bit wild, but it's actually solid in practice. Each site produces three types of artifacts.&lt;/p&gt;

&lt;p&gt;Status snapshot &lt;code&gt;history/{slug}.yml&lt;/code&gt;, for example &lt;code&gt;history/hagi-code-website.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hagicode.com&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;up&lt;/span&gt;
&lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
&lt;span class="na"&gt;responseTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;96&lt;/span&gt;
&lt;span class="na"&gt;lastUpdated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-06-17T00:22:34.485Z&lt;/span&gt;
&lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-03-24T10:07:32.531Z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;shields.io endpoint badge data sources &lt;code&gt;api/{slug}/response-time.json&lt;/code&gt;, &lt;code&gt;uptime.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"response time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"739 ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"yellow"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And response time curve graphs &lt;code&gt;graphs/{slug}/response-time-{day,week,month,year}.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trade-off of this "files as database" approach is well-considered: write-heavy, read-light, controllable scale (about 288 samples per day per site, storing increments rather than full logs), naturally versioned, zero infrastructure. The cost, of course, is that the repository grows continuously and you occasionally need to look back and care about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Events and Notifications: Issues as Event Log
&lt;/h3&gt;

&lt;p&gt;Incident logging relies on GitHub Issues, paired with two built-in repository templates: &lt;code&gt;.github/ISSUE_TEMPLATE/bug_report.md&lt;/code&gt; (user-reported issues) and &lt;code&gt;maintainance-event.md&lt;/code&gt; (scheduled maintenance). The maintenance template uses frontmatter to express the time window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&amp;lt;!--
start: 2021-08-24T13:00:00.220Z
end: 2021-08-24T14:00:00.220Z
expectedDown: google, hacker-news
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upptime parses these Issues and renders "maintenance in progress" and "past events" to the status page and README. Notifications rely on Issues' own watch mechanism, plus configurable webhooks, Slack, Telegram (declare &lt;code&gt;notifications&lt;/code&gt; at the top of &lt;code&gt;.upptimerc.yml&lt;/code&gt;; HagiCode's example repo currently doesn't have this enabled—after all, one less thing is one less thing).&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution: Five Steps to Replicate a Status Page
&lt;/h2&gt;

&lt;p&gt;Replicating a HagiCode-style status page, from zero to live, takes five steps total. Five steps sounds like a lot, but each one is short—take your time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create Repository from Template
&lt;/h3&gt;

&lt;p&gt;Don't &lt;code&gt;git clone&lt;/code&gt; and modify—use GitHub's "Use this template" to create a repository directly (e.g., &lt;code&gt;your-org/upptime&lt;/code&gt;). The template already includes all workflows, Issue templates, and the static site skeleton. After cloning locally, the only thing you need to manually modify is &lt;code&gt;.upptimerc.yml&lt;/code&gt;—leave everything else alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Edit &lt;code&gt;.upptimerc.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Change &lt;code&gt;owner&lt;/code&gt;/&lt;code&gt;repo&lt;/code&gt; to yours, list the addresses you want to monitor in &lt;code&gt;sites&lt;/code&gt;, configure &lt;code&gt;status-website&lt;/code&gt; for the site. The minimum viable version looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-org&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upptime&lt;/span&gt;

&lt;span class="na"&gt;sites&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Main Site&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://example.com&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API Health&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.example.com/health&lt;/span&gt;
    &lt;span class="na"&gt;expectedStatusCodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;

&lt;span class="na"&gt;status-website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status.example.com&lt;/span&gt;   &lt;span class="c1"&gt;# delete if no domain, use default your-org.github.io/upptime&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Example Status&lt;/span&gt;
  &lt;span class="na"&gt;introTitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**Example&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Status**"&lt;/span&gt;
  &lt;span class="na"&gt;introMessage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;服务可用性实时监控&lt;/span&gt;
  &lt;span class="na"&gt;navbar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Status&lt;/span&gt;
      &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub&lt;/span&gt;
      &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/$OWNER/$REPO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advanced items: &lt;code&gt;expectedStatusCodes&lt;/code&gt; limits acceptable status codes (default 200-399); &lt;code&gt;headers&lt;/code&gt; customizes request headers (for endpoints requiring auth); &lt;code&gt;maxResponseTime&lt;/code&gt; marks slow responses. Use these as needed—take what you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure Secrets and Permissions
&lt;/h3&gt;

&lt;p&gt;The workflow defaults to &lt;code&gt;${{ secrets.GH_PAT || github.token }}&lt;/code&gt;. &lt;code&gt;github.token&lt;/code&gt; can run the basic flow, but two limitations will bite you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Workflows triggered by the default token won't trigger downstream workflows (to prevent loops), breaking the "probe → create Issue → notify" chain in the middle.&lt;/li&gt;
&lt;li&gt;Insufficient permissions for cross-repo operations (like multi-org).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Recommend creating a new PAT (requires &lt;code&gt;repo&lt;/code&gt; + &lt;code&gt;workflow&lt;/code&gt; permissions) and storing it as repository Secret &lt;code&gt;GH_PAT&lt;/code&gt;. &lt;code&gt;update-template.yml&lt;/code&gt; has a specific check: without &lt;code&gt;GH_PAT&lt;/code&gt;, it skips template auto-update and prints a warning, so this secret isn't just optional—it's key to peace of mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Enable GitHub Pages
&lt;/h3&gt;

&lt;p&gt;Repository Settings → Pages → Source select &lt;code&gt;Deploy from a branch&lt;/code&gt;, branch select &lt;code&gt;gh-pages&lt;/code&gt;, directory &lt;code&gt;/root&lt;/code&gt;. &lt;code&gt;site.yml&lt;/code&gt; automatically pushes build artifacts to this branch every day at 1 AM. If you configured &lt;code&gt;cname&lt;/code&gt;, add a CNAME record at your DNS provider pointing to &lt;code&gt;your-org.github.io&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's also good to manually trigger once the first time: Actions page find "Static Site CI" → Run workflow, no need to wait foolishly for scheduled tasks—seeing results one second earlier means peace of mind one second earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Verification and Maintenance
&lt;/h3&gt;

&lt;p&gt;After pushing the config, go to Actions and check if "Uptime CI" runs every 5 minutes and if &lt;code&gt;history/&lt;/code&gt; starts showing &lt;code&gt;*.yml&lt;/code&gt; files. The status page address is &lt;code&gt;https://&amp;lt;your-org&amp;gt;.github.io/upptime/&lt;/code&gt; or your custom domain. Later, adding sites or changing domains only requires touching &lt;code&gt;.upptimerc.yml&lt;/code&gt; one file—workflows are fully automated. HagiCode has maintained availability for 14 endpoints using this mechanism for over a year now, basically worry-free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice: I've Walked Through the Potholes for You
&lt;/h2&gt;

&lt;p&gt;The following items are experience accumulated from HagiCode's actual operation—written down so you can take fewer detours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 1: Monitoring Granularity Selection
&lt;/h3&gt;

&lt;p&gt;HagiCode puts web pages (&lt;code&gt;https://hagicode.com&lt;/code&gt;) and pure data endpoints (&lt;code&gt;https://index.hagicode.com/server/index.json&lt;/code&gt;) in the same &lt;code&gt;sites&lt;/code&gt; list. For JSON endpoints, Upptime makes requests and parses HTTP status codes but doesn't validate content structure. If you need deep checks like "returns 200 but content is wrong," you'll need to supplement with &lt;code&gt;expectedStatusCodes&lt;/code&gt; plus external probes—Upptime itself only does black-box HTTP checks—it reads the face, not the mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 2: Clever Use of Response Time Badges
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;api/{slug}/response-time.json&lt;/code&gt; is a &lt;a href="https://shields.io/endpoint" rel="noopener noreferrer"&gt;shields.io endpoint badge&lt;/a&gt; data source. HagiCode's README heavily references these URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FHagiCode-org%2Fupptime%2FHEAD%2Fapi%2Fhagi-code-website%2Fresponse-time.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, you can embed real-time response time badges in any Markdown (project README, blog, third-party pages), with colors driven by the values in &lt;code&gt;message&lt;/code&gt; and the &lt;code&gt;color&lt;/code&gt; field. Note that using &lt;code&gt;HEAD&lt;/code&gt; rather than &lt;code&gt;master&lt;/code&gt;/&lt;code&gt;main&lt;/code&gt; to reference raw files avoids widespread failures after branch renames—details hide stability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 3: Repository Size Control
&lt;/h3&gt;

&lt;p&gt;Sampling every 5 minutes, &lt;code&gt;history/&lt;/code&gt; accumulates considerable volume over a year. Upptime uses incremental YAML rather than full logs, which is relatively restrained, but it's still recommended to occasionally check repository size. If a site's monitoring value declines, remove it from &lt;code&gt;sites&lt;/code&gt;—corresponding historical files can also be manually cleaned up; after all, if you can't bear to delete, the repository will eventually show you what bloating means.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 4: Real Usage of Maintenance Events
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;maintainance-event.md&lt;/code&gt; isn't decoration. Before a planned release, create an Issue using the template, fill in &lt;code&gt;start/end/expectedDown&lt;/code&gt;, and Upptime will mark corresponding sites during that period as "scheduled maintenance," not counting them toward availability statistics, avoiding dragging down the full-year SLA with a normal release. HagiCode's &lt;code&gt;expectedDown&lt;/code&gt; supports comma-separated site name lists, corresponding one-to-one with &lt;code&gt;sites[].name&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 5: Boundaries Between Template Updates and Customization
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Do not edit this file directly!&lt;/code&gt; at the top of all &lt;code&gt;.github/workflows/*.yml&lt;/code&gt; isn't meant to scare you. &lt;code&gt;update-template.yml&lt;/code&gt; overwrites these files with the upstream template weekly. When you need custom behavior, the correct approach is to use officially supported config items in &lt;code&gt;.upptimerc.yml&lt;/code&gt; (like &lt;code&gt;skipTopics&lt;/code&gt;, &lt;code&gt;customStatusWebsite&lt;/code&gt;, &lt;code&gt;runnerSettings&lt;/code&gt;), not to modify workflows. If you really must modify workflows, either turn off &lt;code&gt;update-template.yml&lt;/code&gt; or fork and maintain the template yourself—the latter loses painless upgrades; weigh the trade-offs yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 6: Reality Constraints of Free Quotas
&lt;/h3&gt;

&lt;p&gt;GitHub Actions is free and unlimited for public repositories, which Upptime's design exploits. Private repositories have 2000 free minutes per month, while &lt;code&gt;uptime.yml&lt;/code&gt; runs every 5 minutes for about 1 minute each time—just this one item costs about 8640 minutes per month, exceeding the quota. So the Upptime repository must be public—this is the premise of "free"—don't go private for secrecy and then receive a bill, that's awkward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Returning to the initial question: monitoring a pile of external services—is there really a cheap solution? HagiCode's answer is—yes, and so cheap you'll doubt it's real. Upptime breaks monitoring into four GitHub native components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Probes = GitHub Actions cron&lt;/li&gt;
&lt;li&gt;Database = YAML/JSON files in the repository&lt;/li&gt;
&lt;li&gt;CDN = GitHub Pages&lt;/li&gt;
&lt;li&gt;Event log = GitHub Issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You get: real-time availability, response time curves, historical events, availability badges, custom domains, automatic notifications—all with zero servers and zero monthly fees. The cost is keeping the repository public and occasionally caring about repository size. Compared to hand-rolling a monitoring solution, this cost is actually far lighter.&lt;/p&gt;

&lt;p&gt;The reason this solution works is behind GitHub's ecosystem's sincere subsidy for open-source projects. If you're also maintaining a small multi-site product matrix, I strongly recommend spending an afternoon setting this up—it's far less worry than hand-rolling monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/upptime/upptime" rel="noopener noreferrer"&gt;Upptime official repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://shields.io/endpoint" rel="noopener noreferrer"&gt;shields.io endpoint badge documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule" rel="noopener noreferrer"&gt;GitHub Actions scheduled task documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://status.hagicode.com" rel="noopener noreferrer"&gt;HagiCode status page example&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Around "How to use Upptime to build your own status page for free," a more prudent approach is to gradually get key configurations, dependency boundaries, and landing paths working first, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;Once goals, steps, and acceptance criteria are clear, such solutions can typically enter actual delivery more smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-18-how-to-use-upptime-for-free-status-page%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-18-how-to-use-upptime-for-free-status-page%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>upptime</category>
      <category>githubactions</category>
      <category>githubpages</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>How to Publish an Electron App to Microsoft Store: From MSIX Packaging to Store Submission</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Thu, 18 Jun 2026 02:58:33 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-to-publish-an-electron-app-to-microsoft-store-from-msix-packaging-to-store-submission-3imn</link>
      <guid>https://dev.to/newbe36524/how-to-publish-an-electron-app-to-microsoft-store-from-msix-packaging-to-store-submission-3imn</guid>
      <description>&lt;h1&gt;
  
  
  How to Publish an Electron App to Microsoft Store: From MSIX Packaging to Store Submission
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;At its core, Electron is just an ordinary Win32 desktop application, but the Microsoft Store only recognizes MSIX. This article, drawing from the actual build configuration we used for HagiCode Desktop, breaks down the entire chain from "registering a developer account → creating MSIX packages → submitting to the store" from start to finish. We'll also discuss the pitfalls we encountered along the way—after all, once you've fallen into the traps, they become stories.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;You have an Electron application that needs to be distributed to end users on Windows. Beyond the NSIS installer packages and portable versions we've always used, we also wanted it available in the Microsoft Store. The reasons are actually quite practical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trusted Distribution Channel&lt;/strong&gt;: Apps in the store are signed and reviewed, so users won't be blocked by SmartScreen during installation, and they won't have to face that cold "Unknown publisher" warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Updates and Commercialization&lt;/strong&gt;: The store handles updates for you; subscriptions and permanent licenses can be directly integrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage of Built-in Windows 10/11 Entry Points&lt;/strong&gt;: winget, store search, Start menu recommendations—these entry points are genuinely useful for user acquisition.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, Electron is ultimately not UWP. To publish to the Microsoft Store, the core task is essentially one thing—repackage Electron's output into an &lt;strong&gt;MSIX package&lt;/strong&gt; that the Microsoft Store recognizes, and then complete the registration and submission process dutifully. It sounds simple, but once you actually get started, there are quite a few pitfalls. We spent significant effort mapping out the entire process to fill in these gaps. Below, we'll break down each step in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution described in this article comes from our practice in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode Desktop is an Electron-based desktop application that needs to be distributed to users through three channels: the official website, GitHub Release, and the Microsoft Store. This article explains how we connected the store channel. There's more information about HagiCode at the end—if you're interested, feel free to scroll down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis: Four Critical Questions to Clarify Before Publishing
&lt;/h2&gt;

&lt;p&gt;Publishing to the Microsoft Store involves four key technical judgments. Once you've thought these through clearly, you won't need to repeatedly backtrack later—after all, no one wants to redo their work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Microsoft Store Only Accepts MSIX / AppX, Not Traditional NSIS/EXE
&lt;/h3&gt;

&lt;p&gt;The Microsoft Store's support for desktop applications (Desktop Bridge) is built on the MSIX format. Traditional NSIS installer packages cannot be submitted directly; they must first be repackaged into MSIX using &lt;code&gt;MakeAppx&lt;/code&gt;. Fortunately, Electron Forge provides a &lt;code&gt;@electron-forge/maker-msix&lt;/code&gt; maker that can produce MSIX directly during the packaging phase, saving you the hassle of reverse-engineering packaging from an installed directory.&lt;/p&gt;

&lt;p&gt;Our project includes this maker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@electron-forge/maker-msix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win32&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;appManifest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msixManifestPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;packageAssets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msixAssetsPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;windowsKitPath&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;windowsKitPath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;windowsKitVersion&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;windowsKitVersion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;msixSigningConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key inputs are essentially two: &lt;code&gt;appManifest&lt;/code&gt; (AppxManifest.xml, which defines package identity and capabilities) and &lt;code&gt;packageAssets&lt;/code&gt; (store icon assets). If either of these is wrong, everything else, no matter how well done, is wasted effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Package Identity Must Be Reserved in Partner Center in Advance
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Identity&lt;/code&gt; field (&lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Publisher&lt;/code&gt;) in the MSIX package cannot be filled in arbitrarily—it must match exactly the application identity reserved in Partner Center. Even one character off will result in rejection. Our reserved identity is stored in &lt;code&gt;forge.store-config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hagicode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisherDisplayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CN=8B6C8A94-AAE5-4C8B-9202-A29EA42B042F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"identityName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524.Hagicode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"backgroundColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transparent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"languages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zh-CN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zh-TW"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ja-JP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ko-KR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"de-DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fr-FR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"es-ES"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pt-BR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ru-RU"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;publisher&lt;/code&gt; string comes from the certificate subject issued by Microsoft after developer account registration and must match character-for-character. The &lt;code&gt;identityName&lt;/code&gt; is the package name prefix you reserved. This string must be copied exactly from Partner Center—never type it manually. We'll discuss this again in the "Common Pitfalls" section later.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Desktop Applications Must Declare &lt;code&gt;runFullTrust&lt;/code&gt; Capability
&lt;/h3&gt;

&lt;p&gt;Electron applications need full file system access, need to spawn child processes, and need to run the Node runtime—these can only be achieved in "full trust" mode. Therefore, the MSIX manifest must honestly declare the &lt;code&gt;runFullTrust&lt;/code&gt; capability, otherwise the application will be blocked by the sandbox upon startup, manifesting as various baffling crashes. Our configuration looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"msix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"minVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.17763.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"maxVersionTested"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.19045.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"runFullTrust"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"internetClient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"internetClientServer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"privateNetworkClientsServer"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;runFullTrust&lt;/code&gt; is the standard for desktop application publishing. Setting &lt;code&gt;minVersion&lt;/code&gt; to 17763 (Windows 10 1809) is because from this version onwards, MSIX has stable support for desktop Win32 applications; set it lower and users can't install it; set it higher and you won't cover those older machines.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Store Submission Requires Windows Environment + Microsoft Store CLI
&lt;/h3&gt;

&lt;p&gt;Packaging can be done on cross-platform CI, but store submission (&lt;code&gt;msstore publish&lt;/code&gt;) cannot—it must run the Microsoft Store CLI in a Windows environment with Azure AD application credentials configured. This is why the &lt;code&gt;publish_store&lt;/code&gt; job in the automation pipeline must run on a &lt;code&gt;windows-latest&lt;/code&gt; runner. This is a hard constraint that can't be bypassed, unlike packaging which can be stuffed into a Linux container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution: Eight-Step Process for Complete Store Publishing
&lt;/h2&gt;

&lt;p&gt;Connecting the analysis above, the complete steps to publish an Electron app to the Microsoft Store are roughly as follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Register a Developer Account
&lt;/h3&gt;

&lt;p&gt;First, go to &lt;a href="https://partner.microsoft.com/" rel="noopener noreferrer"&gt;Partner Center&lt;/a&gt; to register a developer account (individual or company) and pay the one-time fee. After the account is activated, you'll receive a &lt;strong&gt;Publisher certificate subject string&lt;/strong&gt;, which looks roughly like this: &lt;code&gt;CN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX&lt;/code&gt;. This is the only source for the &lt;code&gt;publisher&lt;/code&gt; field later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Reserve Application Identity in the Store
&lt;/h3&gt;

&lt;p&gt;Create a new application in Partner Center and fill in the name you want to reserve. The system will assign you an &lt;code&gt;identityName&lt;/code&gt;, which combined with your own Publisher forms the complete package identity. Copy this identity exactly into your local configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;forge.store-config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hagicode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisherDisplayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CN=8B6C8A94-AAE5-4C8B-9202-A29EA42B042F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"identityName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524.Hagicode"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Prepare Store Icon Assets
&lt;/h3&gt;

&lt;p&gt;The Microsoft Store requires a set of PNGs in fixed dimensions: &lt;code&gt;StoreLogo.png&lt;/code&gt;, &lt;code&gt;Square44x44Logo.png&lt;/code&gt;, &lt;code&gt;Square150x150Logo.png&lt;/code&gt;, &lt;code&gt;Wide310x150Logo.png&lt;/code&gt;, etc. Our &lt;code&gt;prepare-msix.js&lt;/code&gt; script validates whether all these assets are present before packaging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Validate required store icon assets, missing even one won't work&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requiredAssets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;StoreLogo.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Square44x44Logo.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Square150x150Logo.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Wide310x150Logo.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetName&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;requiredAssets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;generatedAssetsPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;assetName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assetPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Missing required MSIX asset after preparation: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;assetPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why do this? Because if one size is missing, MakeAppx won't tell you exactly what's wrong during packaging, and you'll only get rejected during store review—by which time you've already waited several days. Validating in advance is an effective defense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Generate AppxManifest.xml
&lt;/h3&gt;

&lt;p&gt;The manifest needs to include package identity, capabilities, visual assets, and the entry executable. We use an override configuration (&lt;code&gt;forge.store-config.json&lt;/code&gt;) to drive &lt;code&gt;prepare-msix.js&lt;/code&gt; to generate the manifest, ensuring the identity matches the store. The key sections of the manifest look roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Package identity: must match Partner Center --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Identity&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"newbe36524.Hagicode"&lt;/span&gt;
          &lt;span class="na"&gt;Publisher=&lt;/span&gt;&lt;span class="s"&gt;"CN=8B6C8A94-AAE5-4C8B-9202-A29EA42B042F"&lt;/span&gt;
          &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.2.3.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;Applications&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Application&lt;/span&gt; &lt;span class="na"&gt;Id=&lt;/span&gt;&lt;span class="s"&gt;"Hagicode"&lt;/span&gt; &lt;span class="na"&gt;Executable=&lt;/span&gt;&lt;span class="s"&gt;"Hagicode.exe"&lt;/span&gt; &lt;span class="na"&gt;EntryPoint=&lt;/span&gt;&lt;span class="s"&gt;"Windows.FullTrustApplication"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;uap:VisualElements&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Application&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Applications&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Capability declaration: runFullTrust is key for desktop apps --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Capabilities&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;rescap:Capability&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"runFullTrust"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Capability&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"internetClientServer"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Capabilities&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that line &lt;code&gt;EntryPoint="Windows.FullTrustApplication"&lt;/code&gt;—this is a critical marker for desktop applications. Combined with the &lt;code&gt;runFullTrust&lt;/code&gt; capability, it allows the app to run with full permissions. Without it, the app is confined to the sandbox, quite frustratingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Package with maker-msix
&lt;/h3&gt;

&lt;p&gt;The build command is in &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:win:store"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run generate:store-bindings &amp;amp;&amp;amp; node scripts/build-store-package.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It ultimately calls Electron Forge, passing &lt;code&gt;forge.store-config.json&lt;/code&gt; as an override configuration. The maker-msix will call the Windows SDK's &lt;code&gt;MakeAppx&lt;/code&gt; to output an &lt;code&gt;.msix&lt;/code&gt; file. There's a hard constraint here: &lt;strong&gt;packaging must be done on Windows&lt;/strong&gt; (or in a container with Windows SDK), since it depends on &lt;code&gt;MakeAppx&lt;/code&gt;—this can't be bypassed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Sign (Can Skip for Store Submission)
&lt;/h3&gt;

&lt;p&gt;This step is easily overlooked—packages submitted to the store are re-signed by Microsoft with their own certificates, so for development self-testing outside "formal submission," you don't need to sign. However, if you want to install and test locally, you need to sign with a trusted certificate, otherwise Windows will refuse installation. Our &lt;code&gt;resolveMsixSigningConfig&lt;/code&gt; returns an empty object when no signing materials are configured, letting the process continue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't sign if no signing materials configured, let the store re-sign uniformly&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveMsixSigningConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MSIX_CERT_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;signMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signtool&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;certFilePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MSIX_CERT_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;certPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MSIX_CERT_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separating the "self-testing signing" and "submitting unsigned" paths is a key practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Configure Microsoft Store CLI Credentials
&lt;/h3&gt;

&lt;p&gt;Create an Azure AD application in the Azure portal, grant it permissions to access Partner Center, and obtain the following set of credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AZURE_AD_APPLICATION_CLIENT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_AD_APPLICATION_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_AD_TENANT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELLER_ID&lt;/code&gt; (Seller ID from Partner Center)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MICROSOFT_STORE_PRODUCT_ID&lt;/code&gt; (Product ID of the reserved application)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This step is slightly convoluted, but the documentation for both Azure Portal and Partner Center is quite detailed—just follow it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Submit to Store
&lt;/h3&gt;

&lt;p&gt;In a Windows environment, submit using the Microsoft Store CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Configure credentials&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;msstore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reconfigure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--tenantId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_AD_TENANT_ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--clientId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_AD_APPLICATION_CLIENT_ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--clientSecret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_AD_APPLICATION_SECRET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--sellerId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;SELLER_ID&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Submit MSIX package to the reserved product&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;msstore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$packagePath&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;MICROSOFT_STORE_PRODUCT_ID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After submission, you need to return to Partner Center to fill in the store listing details (description, screenshots, pricing, rating), then click submit for review. Review typically takes 1–3 business days; first submissions tend to take longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice: Consolidating Configuration and Pitfall Experience
&lt;/h2&gt;

&lt;p&gt;After going through the process once, these practices can help you avoid some detours—after all, after you've taken enough detours, they don't feel like detours anymore, but some things can still be skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store Configuration Files Separately
&lt;/h3&gt;

&lt;p&gt;Separating "general build configuration" from "store-specific configuration" is key. Our approach is: &lt;code&gt;forge.config.js&lt;/code&gt; handles daily builds (NSIS, portable, macOS dmg), while &lt;code&gt;forge.store-config.json&lt;/code&gt; only extends and overrides during store builds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forge.config.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reserved&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"msix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"minVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.17763.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"maxVersionTested"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.19045.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"runFullTrust"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internetClient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internetClientServer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"privateNetworkClientsServer"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, store builds and release builds don't pollute each other. HagiCode Desktop maintains three distribution channels simultaneously; configuration separation is the prerequisite for our stable iteration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Numbers Must Be Four Segments
&lt;/h3&gt;

&lt;p&gt;MSIX version numbers must be four segments (&lt;code&gt;Major.Minor.Build.Revision&lt;/code&gt;, e.g., &lt;code&gt;1.2.3.0&lt;/code&gt;), but Electron's &lt;code&gt;package.json&lt;/code&gt; typically only has three segments. That &lt;code&gt;buildVersion&lt;/code&gt; field is used to fill in the last segment—when submitting to the store, version numbers must increment, and the fourth segment is very convenient for distinguishing multiple submissions under the same semantic version. Those who've encountered this will understand; those who haven't will eventually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Language Declaration
&lt;/h3&gt;

&lt;p&gt;The store supports multi-language listings, which in the manifest corresponds to each &lt;code&gt;&amp;lt;Resource Language="..." /&amp;gt;&lt;/code&gt; tag. We declared ten languages, and the store requires filling in a description for each (you can use machine translation to pass review first, then localize gradually). The corresponding rendering logic in &lt;code&gt;prepare-msix.js&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Render language list as Resource tags in MSIX manifest&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderResourceTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;languages&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`    &amp;lt;Resource Language="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeXml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;" /&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Common Pitfalls (Focus Here)
&lt;/h3&gt;

&lt;p&gt;HagiCode Desktop has encountered almost every one of these pitfalls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Publisher Mismatch&lt;/strong&gt;: When copying the publisher string from Partner Center, it's easy to accidentally lose a space or mess up the case, leading to immediate rejection.建议直接写成配置文件，别手敲。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing &lt;code&gt;runFullTrust&lt;/code&gt;&lt;/strong&gt;: After the app starts, it can't access the file system or spawn child processes, manifesting as various bizarre crashes that are frustrating to debug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete Icon Sizes&lt;/strong&gt;: MakeAppx doesn't validate, but store review will reject it. &lt;code&gt;prepare-msix.js&lt;/code&gt; validating in advance is an effective defense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-Incrementing Version Numbers&lt;/strong&gt;: The store refuses to accept the same or lower version numbers; CI pipelines must ensure bumping on each build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running maker-msix in Non-Windows Environments&lt;/strong&gt;: It won't find &lt;code&gt;MakeAppx&lt;/code&gt;; you must use a &lt;code&gt;windows-latest&lt;/code&gt; runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing Confusion&lt;/strong&gt;: Use self-signed certificates for self-testing, submit unsigned for store submission and let Microsoft re-sign—separate these two paths, don't stuff self-signed certificates into submission packages.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Automation Recommendations
&lt;/h3&gt;

&lt;p&gt;After manually going through the entire process once and understanding each step, it's strongly recommended to connect GitHub Actions for automation. We eventually chained version parsing, MSIX building, GitHub Release publishing, and store publishing into a pipeline, checking for new versions every 4 hours. These details are fully broken down in our other article "Automating Windows App Publication to Microsoft Store."&lt;/p&gt;

&lt;p&gt;If you just want to get your app on the store first and integrate commercialization (subscriptions / permanent licenses) later, you can also check out our "How to Integrate Microsoft Store Subscriptions and Permanent Licenses in Electron Desktop Apps"—that article covers connecting commercialization capabilities after store publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/AppPublisher" rel="noopener noreferrer"&gt;Microsoft Store CLI Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.electronforge.io/config/makers/maker-msix" rel="noopener noreferrer"&gt;electron-forge maker-msix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/windows/msix/" rel="noopener noreferrer"&gt;MSIX Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Regarding "How to Publish an Electron App to Microsoft Store: From MSIX Packaging to Store Submission," a more prudent approach is to first run through key configurations, dependency boundaries, and implementation paths step by step, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;Once objectives, steps, and acceptance criteria are clear, such solutions can typically enter actual delivery more smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-18-electron-app-publish-to-microsoft-store%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-18-electron-app-publish-to-microsoft-store%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>electron</category>
      <category>microsoftstore</category>
      <category>msix</category>
      <category>windows</category>
    </item>
    <item>
      <title>How to Call Windows Native APIs in Electron</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Wed, 17 Jun 2026 03:22:36 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-to-call-windows-native-apis-in-electron-iaa</link>
      <guid>https://dev.to/newbe36524/how-to-call-windows-native-apis-in-electron-iaa</guid>
      <description>&lt;h1&gt;
  
  
  How to Call Windows Native APIs in Electron
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Calling Windows native APIs in an Electron app feels like wanting to see the ocean but only having a map. After some trial and error, I've found a few paths—writing this article serves as a record and a guide for others who might follow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;When building Electron desktop applications, you inevitably need to interact with the operating system. On Windows, these requirements are quite common:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calling Windows Store APIs for in-app purchases&lt;/li&gt;
&lt;li&gt;Handling file system virtualization specific to Windows Store apps&lt;/li&gt;
&lt;li&gt;Obtaining system-level permissions and resources&lt;/li&gt;
&lt;li&gt;Interacting with Windows Runtime (WinRT) components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Electron is fundamentally a Node.js environment, and Node.js doesn't natively provide direct access to Windows native APIs. A bridge is needed between the two.&lt;/p&gt;

&lt;p&gt;It's like trying to communicate with a friend who doesn't speak Chinese—you need a translator. Electron is written in JavaScript, Windows APIs in C/C++. The language barrier requires building a bridge. That's the harsh reality of the code world.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solutions shared in this article come from our practical experience with the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode Desktop needs to call Microsoft Store APIs to handle subscription purchases and license management, which is why we developed a set of technical solutions. After all, necessity drives innovation—that's a truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Solution Comparison
&lt;/h2&gt;

&lt;p&gt;When calling Windows native APIs in Electron, there are several mainstream approaches to choose from. Each has its applicable scenarios—like different tools in a toolbox, they work best when used in the right place, otherwise just add trouble.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;Applicable Scenarios&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;dynwinrt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WinRT APIs (e.g., Store API)&lt;/td&gt;
&lt;td&gt;Type-safe, auto-generated bindings, modern JavaScript support&lt;/td&gt;
&lt;td&gt;Only supports WinRT APIs, requires Windows SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Native Node.js Extensions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High performance, any Windows API&lt;/td&gt;
&lt;td&gt;Complete control, optimal performance&lt;/td&gt;
&lt;td&gt;Requires C++ development skills, complex cross-platform compatibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;child_process + PowerShell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Temporary, one-off calls&lt;/td&gt;
&lt;td&gt;Simple and fast, no compilation needed&lt;/td&gt;
&lt;td&gt;Poor performance, complex error handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;edge.js/ffi-napi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Calling existing DLLs&lt;/td&gt;
&lt;td&gt;Can reuse existing libraries&lt;/td&gt;
&lt;td&gt;Compatibility issues, high maintenance cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;HagiCode Desktop adopts a hybrid approach: using dynwinrt to access Windows Store APIs, native Node.js extensions for high-performance Store purchase operations, and Node.js's native fs and path modules to handle Windows Store app-specific file system virtualization. Keep it simple when possible—that's our principle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution 1: Using dynwinrt to Call WinRT APIs
&lt;/h2&gt;

&lt;p&gt;dynwinrt is a toolchain provided by Microsoft that can automatically generate JavaScript bindings based on Windows SDK metadata files. It's specifically designed for calling WinRT APIs, like Windows Store APIs.&lt;/p&gt;

&lt;p&gt;Install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"optionalDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@microsoft/dynwinrt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0-preview.6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@microsoft/dynwinrt-codegen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0-preview.6"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate WinRT bindings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/generate-store-bindings.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStoreNamespace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;windowsWinmdPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dynwinrt-codegen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--winmd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;windowsWinmdPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--namespace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Windows.Services.Store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/main/subscription/generated-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use generated bindings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Using Store API bindings generated by dynwinrt&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Windows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../subscription/generated-js/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;queryStoreProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storeContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Windows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StoreContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;storeContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAssociatedStoreProductsAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Subscription&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Durable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extendedError&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Store API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extendedError&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;dynwinrt's advantage is type safety, and the generated code aligns with modern JavaScript conventions. But it can only handle WinRT APIs—if you need to call traditional Win32 APIs, you'll need other solutions. That's how tools work—each has its strengths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution 2: Native Node.js Extensions
&lt;/h2&gt;

&lt;p&gt;When high performance is needed or dynwinrt doesn't support the functionality, native Node.js extensions are the best choice. This approach requires writing C++ code, then compiling it into .node files using node-gyp.&lt;/p&gt;

&lt;p&gt;Create binding.gyp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;targets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;windows-store-addon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/windows-store-addon.cpp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_dirs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;!(node -e &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;require(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WIN32_LEAN_AND_MEAN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C++ native module example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/windows-store-addon.cpp&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;nan.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;windows.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;wrl.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;windows.services.store.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="n"&gt;v8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="n"&gt;Windows&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;NAN_METHOD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QueryStoreStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Nan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AsyncWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[]()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Call Windows Store API&lt;/span&gt;
      &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StoreContext&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetAssociatedStoreProductsAsync&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetResults&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="c1"&gt;// Process results&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;Nan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AsyncQueueWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;NAN_MODULE_INIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InitModule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Nan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Nan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"queryStoreStatus"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ToLocalChecked&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
           &lt;span class="n"&gt;Nan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Nan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FunctionTemplate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QueryStoreStatus&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;ToLocalChecked&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;NODE_MODULE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;windows_store_addon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InitModule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile and use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node-gyp rebuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;addon&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./build/Release/windows-store-addon.node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;addon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryStoreStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;storeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-store-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;productKinds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Subscription&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Durable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native extensions offer the best performance, but development costs are high. You need C++ skills and must handle cross-platform compatibility issues. If your team has C++ experience or performance requirements are particularly demanding, this solution is worth the investment. But this path is ultimately more challenging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution 3: Handling Windows Store App Virtualization
&lt;/h2&gt;

&lt;p&gt;Windows Store apps run in a virtualized environment, requiring special path mapping. HagiCode Desktop uses the following function to handle this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/main/windows-store-path-display.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveWindowsStorePackageFamilyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;executablePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WINDOWS_APPS_SEGMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;windowsapps&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;windowsPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;executablePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markerIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;windowsPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WINDOWS_APPS_SEGMENT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markerIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;relativePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;windowsPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markerIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;WINDOWS_APPS_SEGMENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;packageFullName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;packageFullName&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveWindowsStoreVirtualizedPhysicalPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;logicalPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResolveWindowsStorePathDisplayOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;packageFamilyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;packageFamilyName&lt;/span&gt;
    &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;resolveWindowsStorePackageFamilyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execPath&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;packageFamilyName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;packageStorageRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;win32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Packages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;packageFamilyName&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Map virtualized path to physical path&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPathWithinWindowsRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logicalPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APPDATA&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;win32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;packageStorageRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LocalCache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Roaming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;win32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APPDATA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logicalPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Virtualization is quite complex. Simply put, the file paths that Windows Store apps see differ from actual storage locations, requiring translation. The code above performs this translation. Like memory and reality, sometimes they don't align and require patience to distinguish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Experience
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Platform Detection
&lt;/h3&gt;

&lt;p&gt;Always check &lt;code&gt;process.platform === 'win32'&lt;/code&gt; to avoid executing Windows-specific code on non-Windows platforms. This is a good habit—like checking the weather before leaving home to avoid getting caught in the rain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win32&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not-supported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;

&lt;p&gt;Windows API calls may fail and need proper error handling. We've learned this the hard way—without comprehensive error handling, users have no idea what went wrong when problems occur. Actually, writing more code teaches you that error handling isn't for anyone else—it's just to save yourself trouble.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeThrownError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;errorCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorWithCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;errorCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeErrorCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorWithCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;errorCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Async Handling
&lt;/h3&gt;

&lt;p&gt;Most Windows Store APIs are asynchronous—use Promise or async/await. When writing async code, remember to handle edge cases like timeouts and cancellation. The taste of waiting—no one wants more of that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;queryStatus&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RawStoreLicenseState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;storeContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAssociatedStoreProductsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productKinds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;buildSupportedStateFromProductQueries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;buildUnavailableState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Resource Cleanup
&lt;/h3&gt;

&lt;p&gt;Ensure native resources are released when no longer needed. C++ resources aren't automatically collected—manual release is a good habit. Like some things, you can only travel light after letting them go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MicrosoftStoreSubscriptionBroker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;broker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StoreLicensePlatformBroker&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;broker&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;broker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Timestamp Conversion
&lt;/h3&gt;

&lt;p&gt;Windows uses 1601-01-01 as its epoch, which needs conversion to Unix timestamps. This detail is easily overlooked, but if handled incorrectly, dates will be completely wrong. Time—a small difference makes a big difference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WINDOWS_EPOCH_OFFSET_MILLISECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;11644473600000&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HUNDRED_NANOSECONDS_PER_MILLISECOND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toIsoDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;universalTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;universalTime&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;universalTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ticks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;universalTime&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bigint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;universalTime&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticks&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unixMilliseconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ticks&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;HUNDRED_NANOSECONDS_PER_MILLISECOND&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;WINDOWS_EPOCH_OFFSET_MILLISECONDS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unixMilliseconds&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Best Practices
&lt;/h3&gt;

&lt;p&gt;Based on our experience with the HagiCode project, here are a few recommendations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prioritize dynwinrt: For WinRT APIs, dynwinrt provides type-safe and modern JavaScript bindings&lt;/li&gt;
&lt;li&gt;Minimize native extensions: Only use native extensions when truly necessary for high performance or unsupported features&lt;/li&gt;
&lt;li&gt;Cross-platform compatibility: Use conditional compilation or runtime detection for different platforms&lt;/li&gt;
&lt;li&gt;Test coverage: Thoroughly test native API calls on Windows, including error scenarios&lt;/li&gt;
&lt;li&gt;Documentation: Clearly document the purpose and potential side effects of each native API call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When writing code, keep it simple if possible. If dynwinrt can solve the problem, don't write C++ extensions. Maintenance costs will be much lower. This is a small insight—not some profound truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Calling Windows native APIs is an important means for Electron apps to implement advanced features on Windows. This article shares several technical solutions used in the HagiCode Desktop project: dynwinrt for WinRT APIs, native Node.js extensions for high-performance scenarios, and virtualized path handling for Store app file access.&lt;/p&gt;

&lt;p&gt;Which solution to choose depends on your specific needs. If you're only calling WinRT APIs, dynwinrt is the simplest choice. If you need high performance or traditional Win32 APIs, native extensions are essential. For temporary operations, using child_process to call PowerShell works too. All roads lead to Rome—some are easier to travel, others a bit more winding.&lt;/p&gt;

&lt;p&gt;Regardless of which solution you use, remember these principles: proper platform detection, comprehensive error handling, careful async handling, timely resource cleanup. These details determine the robustness of your code. Writing code for long enough teaches you that details matter more than grand frameworks.&lt;/p&gt;

&lt;p&gt;If you're working on similar development, I hope these experiences help you. Technology—you gain experience after stumbling enough. Like life—you learn to walk after falling enough...&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/uwp/api/windows.services.store" rel="noopener noreferrer"&gt;Windows.Services.Store namespace - WinRT Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/api/n-api.html#n_api_napi_create_threadsafe_function" rel="noopener noreferrer"&gt;Node-API ThreadSafeFunction Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.electronjs.org/docs/latest/" rel="noopener noreferrer"&gt;Electron Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;A more steady approach to "How to Call Windows Native APIs in Electron" is to first validate key configurations, dependency boundaries, and implementation paths, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;Once objectives, steps, and acceptance criteria are clear, such solutions typically enter actual delivery more smoothly.&lt;/p&gt;

</description>
      <category>electron</category>
      <category>windows</category>
      <category>node</category>
    </item>
    <item>
      <title>Integrating Microsoft Store Subscription and Permanent Licenses in Electron Desktop Apps</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Tue, 16 Jun 2026 12:26:16 +0000</pubDate>
      <link>https://dev.to/newbe36524/integrating-microsoft-store-subscription-and-permanent-licenses-in-electron-desktop-apps-3dlf</link>
      <guid>https://dev.to/newbe36524/integrating-microsoft-store-subscription-and-permanent-licenses-in-electron-desktop-apps-3dlf</guid>
      <description>&lt;h1&gt;
  
  
  Integrating Microsoft Store Subscription and Permanent Licenses in Electron Desktop Apps
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;When your Electron app needs to sell subscriptions and perpetual licenses through Microsoft Store, how do you cleanly integrate that WinRT commercial API into your business logic? This is an old story—we stumbled and sweated through it in HagiCode Desktop, eventually figuring out this layered approach. Writing it down as a roadmap for those who come after.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;HagiCode Desktop is an Electron application distributed through Microsoft Store. Commercially, there are only two product types: one is the Sponsor Plan (sponsor subscription, Store ID &lt;code&gt;9N0BTGWV23M1&lt;/code&gt;), renewing monthly or annually—like a relationship that needs constant watering. The other is TurboEngine (perpetual license DLC, Store ID &lt;code&gt;9NSD809W18Z6&lt;/code&gt;), a one-time purchase—like that old book on the shelf you never opened again, but it's yours nonetheless.&lt;/p&gt;

&lt;p&gt;The problem is, the Electron runtime itself doesn't have the ability to directly call Microsoft Store commercial APIs. Store purchases and license queries all rely on WinRT's &lt;code&gt;Windows.Services.Store&lt;/code&gt; namespace, and these APIs can only be used in native code. But the Electron main process is in a Node.js environment—you can't &lt;code&gt;import&lt;/code&gt; a WinRT type there—it's like trying to hold moonlight in your hand, but coming up empty.&lt;/p&gt;

&lt;p&gt;Even more troublesome is that commercialization status isn't something you can just check once and feel at peace. Users might unsubscribe, renew, or switch devices in the Store client, and the app's feature toggles need to change accordingly. Making users click "refresh" every time makes for a poor experience, but querying too frequently hits Store rate limits—network jitters and a perfectly good subscription gets reported as "not subscribed," locking out paid users' features. Building something like that makes you want to laugh to hide the tears.&lt;/p&gt;

&lt;p&gt;There's another easily overlooked corner: different distribution channels behave differently. Non-Store versions (like portable builds) don't have the Store runtime at all—calling &lt;code&gt;StoreContext&lt;/code&gt; will fail outright. In such cases, the app shouldn't crash, nor should it pretend users have subscriptions. You need to give a clear "unsupported" status. After all, pretending to have something is sadder than honestly admitting you don't.&lt;/p&gt;

&lt;p&gt;For these reasons, we built a layered architecture. This approach later crystallized into two HagiCode OpenSpec proposals: &lt;code&gt;desktop-subscription-entitlements&lt;/code&gt; (subscription license persistence, standardization, and entitlement derivation) and &lt;code&gt;desktop-turboengine-msstore-license&lt;/code&gt; (TurboEngine perpetual license purchase, refresh, and DLC injection). More on these below.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practice in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI coding assistant project spanning multiple platforms: Web, Desktop, CLI, and more. HagiCode Desktop is the desktop product line discussed in this article, and the complete source code is available at &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layering is Key
&lt;/h2&gt;

&lt;p&gt;Writing Store calls directly in the Electron main process gets messy. WinRT async objects, COM threading models, window handle passing—mixing these with business logic makes the code nearly unmaintainable. Our approach is to slice the entire pipeline into four layers, each shouldering one responsibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Renderer Process (React)
   ↕  IPC bridge
Electron Main Process (TypeScript)
   ↕  broker interface
Native Node Addon (C++)
   ↕  WinRT
Windows.Services.Store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the bottom is a C++ native addon named &lt;code&gt;hagicode_store_purchase_addon.node&lt;/code&gt;. It exposes only two methods: &lt;code&gt;requestPurchase(storeId, windowHandle)&lt;/code&gt; and &lt;code&gt;queryStoreStatus(storeId, productName, productKinds)&lt;/code&gt;. These correspond to WinRT's &lt;code&gt;RequestPurchaseAsync&lt;/code&gt; and &lt;code&gt;GetAssociatedStoreProductsAsync&lt;/code&gt; / &lt;code&gt;GetUserCollectionAsync&lt;/code&gt;. The addon's entire job is to convert WinRT async results to JSON and send them back to the JavaScript thread via &lt;code&gt;Napi::ThreadSafeFunction&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the middle is a TypeScript &lt;code&gt;StoreLicenseService&lt;/code&gt;. It doesn't care about WinRT, only about business semantics: refresh, retry, cache, entitlement derivation, and status broadcasting. It communicates with the underlying layer through a &lt;code&gt;StoreLicensePlatformBroker&lt;/code&gt; interface with just three methods: &lt;code&gt;queryStatus()&lt;/code&gt;, &lt;code&gt;purchase()&lt;/code&gt;, and &lt;code&gt;dispose()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At the top are &lt;code&gt;SubscriptionService&lt;/code&gt; and &lt;code&gt;TurboEngineLicenseService&lt;/code&gt;, which are essentially thin wrappers around &lt;code&gt;StoreLicenseService&lt;/code&gt;, each bound to specific product configurations (Store ID, product name, entitlement names).&lt;/p&gt;

&lt;p&gt;This layering brings a direct benefit: subscriptions and perpetual licenses can share the same engine. &lt;code&gt;StoreLicenseService&lt;/code&gt; is a generic class parameterized by snapshot type and entitlement name. Adding a new product only requires writing another &lt;code&gt;StoreLicenseProductConfig&lt;/code&gt;—no need to copy-paste the entire service. If HagiCode later needs to integrate macOS's StoreKit or other commercialization channels, theoretically only the broker implementation needs to change—no business layer code needs to move. This is the gentle power of layering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standardization: Cleaning Store's Dirty Data
&lt;/h2&gt;

&lt;p&gt;The data returned by WinRT is quite "raw." &lt;code&gt;StoreProductQueryResult&lt;/code&gt; contains nested &lt;code&gt;IVectorView&lt;/code&gt; and &lt;code&gt;IMap&lt;/code&gt;, SKU's &lt;code&gt;CollectionData.EndDate&lt;/code&gt; is in Windows DateTime ticks (starting from 1601, in 100-nanosecond units), and error codes are HRESULTs. If you throw these directly at the renderer process, the frontend code will likely collapse.&lt;/p&gt;

&lt;p&gt;So the broker layer does standardization, flattening the raw WinRT objects into &lt;code&gt;RawStoreLicenseState&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;RawStoreLicenseState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;supported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;store-unavailable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;appLicenseActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RawStoreLicenseProduct&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RawStoreLicenseSku&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;license&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RawStoreLicense&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;purchaseEligibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;licensable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not-licensable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;license-action-not-applicable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;network-error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server-error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;errorCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a detail worth mentioning: the query actually uses two Store calls. One is &lt;code&gt;GetAssociatedStoreProductsAsync&lt;/code&gt; (products associated with the current app), the other is &lt;code&gt;GetUserCollectionAsync&lt;/code&gt; (products the user already owns). The reason is simple: subscription products might appear in the association list but the user hasn't bought them yet, or they might already be in the user's collection. Only by cross-referencing the two results can you accurately determine "whether owned"—it's like looking at a person from two angles to avoid misjudgment.&lt;/p&gt;

&lt;p&gt;The ticks-to-ISO date conversion is worth noting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WINDOWS_EPOCH_OFFSET_MILLISECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;11644473600000&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HUNDRED_NANOSECONDS_PER_MILLISECOND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ticks are in 100-nanosecond units from 1601; convert to milliseconds, then subtract the Windows/Unix epoch difference&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unixMilliseconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;ticks&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;HUNDRED_NANOSECONDS_PER_MILLISECOND&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;WINDOWS_EPOCH_OFFSET_MILLISECONDS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;11644473600000&lt;/code&gt; is the number of milliseconds between 1601-01-01 and 1970-01-01. This conversion is also done in the C++ addon (using &lt;code&gt;FileTimeToSystemTime&lt;/code&gt;), and both sides' results must match—otherwise you get bizarre misalignments like "main process sees today, addon sees yesterday." When time (like relationships) is misaligned, nothing makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Machine: From "Raw Data" to "Business State"
&lt;/h2&gt;

&lt;p&gt;After standardization, we need another layer of abstraction. Business code doesn't actually need to know what &lt;code&gt;purchaseEligibility&lt;/code&gt; is—it only cares "is the subscription actually valid." The &lt;code&gt;deriveStatus&lt;/code&gt; function in &lt;code&gt;normalize.ts&lt;/code&gt; does this translation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deriveStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RawStoreLicenseState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;productConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StoreLicenseProductConfig&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;StoreLicenseStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;supported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expirationDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;expirationDate&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;collectionEndDate&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expirationTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;expirationDate&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expirationDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;NaN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasExpired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expirationTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;expirationTime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isOwned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;license&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isInUserCollection&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isInUserCollection&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isOwned&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasExpired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasExpired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...other branches: inactive / canceled / grace-period / pending&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final business states are seven: &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;inactive&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt;, &lt;code&gt;canceled&lt;/code&gt;, &lt;code&gt;grace-period&lt;/code&gt;, &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;unknown&lt;/code&gt;. The renderer process only looks at this one field, never touching the raw data again.&lt;/p&gt;

&lt;p&gt;There's a design trade-off here: the &lt;code&gt;active&lt;/code&gt; determination doesn't check whether &lt;code&gt;expirationDate&lt;/code&gt; exists. The reason is simple—perpetual licenses (TurboEngine) don't have expiration dates at all; Store returning &lt;code&gt;license.isActive&lt;/code&gt; as true is sufficient. If we insist "must have expiration date to be active," we'd incorrectly judge buyout users as unsubscribed—which would be too hurtful. This detail is explicitly stated in the spec: perpetual licenses remain active when no expiration metadata exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fault Tolerance: Don't Lose Subscriptions When Network is Poor
&lt;/h2&gt;

&lt;p&gt;Store APIs return errors or time out during network jitter. If we clear the state on every failure, paid users' permissions will frequently drop—this is obvious, but it does happen. HagiCode's strategy is "preserve last known good state on failure, mark as stale."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;StoreLicenseService.refresh&lt;/code&gt; has an internal retry loop (default 3 times, 350ms interval) and does "status regression" detection: if the previous state was active but this query returns something not active, treat it as a temporary error and retry rather than accepting this degraded result directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;getRetryReason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;recoverySnapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TSnapshot&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;store-unavailable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status-regression&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;supported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;store-unavailable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recoverySnapshot&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status-regression&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after all retries fail will it use &lt;code&gt;createStaleSnapshot&lt;/code&gt; to return the last good state marked as stale, accompanied by a &lt;code&gt;store-refresh-failed&lt;/code&gt; diagnostic. The renderer process can decide whether to disable features in stale state—typically, continue allowing access, giving users a buffer. After all, no one wants to be unable to use something they paid for just because the network was bad that day.&lt;/p&gt;

&lt;p&gt;Another detail is &lt;code&gt;refreshInFlight&lt;/code&gt; deduplication. If a refresh is already in progress, new refresh calls reuse the same Promise, avoiding concurrent requests overwhelming the Store—the principle is the same as queuing:挤成一团反而谁也过不去。&lt;/p&gt;

&lt;h2&gt;
  
  
  Entitlement Derivation: Decoupling Status and Feature Toggles
&lt;/h2&gt;

&lt;p&gt;Subscription status answers "is the subscription valid," but feature toggles care about "can the user use a specific feature." These aren't one-to-one correspondences. An active subscription might correspond to multiple entitlements (sponsor badge, premium feature gates), and future plans might include tier-based differentiation.&lt;/p&gt;

&lt;p&gt;So we added an &lt;code&gt;EntitlementEvaluator&lt;/code&gt; layer in between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TSnapshot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;TEntitlement&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;supported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeEntitlements&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the subscription product configuration, we declare which entitlements it grants when activated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscriptionEntitlementNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sponsorBadge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;premiumFeatureGate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, feature code depends only on the &lt;code&gt;entitlements&lt;/code&gt; array, not directly reading &lt;code&gt;status&lt;/code&gt;. Future tier additions or entitlement splits only require changing configuration and the evaluator—consumer code doesn't need to change. This decoupling is especially important in multi-product-line projects like HagiCode—subscriptions and perpetual licenses share the same entitlement model, and the frontend only needs to query one array. The world suddenly feels much cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime Degradation: What If No Store?
&lt;/h2&gt;

&lt;p&gt;Calling the addon from non-Store distributed versions (portable builds, development environments) will fail. HagiCode uses lazy initialization and degradation in &lt;code&gt;MicrosoftStoreSubscriptionBroker&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;initializeBroker&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StoreLicensePlatformBroker&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBroker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapterFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;windowHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If Store runtime isn't found, degrade to a "supports nothing" broker&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBroker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnavailableSubscriptionPlatformBroker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UnavailableSubscriptionPlatformBroker&lt;/code&gt; implements the same interface, but its &lt;code&gt;queryStatus&lt;/code&gt; always returns &lt;code&gt;store-unavailable&lt;/code&gt;, and &lt;code&gt;purchase&lt;/code&gt; always returns &lt;code&gt;not-supported&lt;/code&gt;. Upper-layer code is completely unaware—it just becomes "unsupported" status, and the renderer shows a "Please get through Microsoft Store" prompt based on that.&lt;/p&gt;

&lt;p&gt;This design allows the entire commercialization module to run safely under any distribution channel without crashing due to missing Store runtime. If you're also building a multi-channel Electron app, this is particularly worth copying—don't let "environment not supported" become a crash. Some things, when admitted honestly, are actually more dignified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Startup Flow and IPC Channels
&lt;/h2&gt;

&lt;p&gt;On app startup, &lt;code&gt;main.ts&lt;/code&gt; decides whether to initialize the subscription service based on the &lt;code&gt;--desktop-subscription-enabled=1&lt;/code&gt; parameter. This parameter is only included in the Store version's startup command, avoiding wasted loading in non-Store versions—every bit of effort saved is worth it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initializeSubscriptionService&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;subscriptionFeatureEnabled&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;subscriptionService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;subscriptionService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SubscriptionService&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;broker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MicrosoftStoreSubscriptionBroker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;windowHandle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mainWindow&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getNativeWindowHandle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;entitlementEvaluator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EntitlementEvaluator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;registerSubscriptionHandlers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;subscriptionService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;getWindows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ElectronBrowserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllWindows&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;windowHandle&lt;/code&gt; comes from &lt;code&gt;mainWindow.getNativeWindowHandle()&lt;/code&gt;. This Buffer is parsed into a &lt;code&gt;bigint&lt;/code&gt; and passed to the native addon, which then uses it to call &lt;code&gt;IInitializeWithWindow::Initialize&lt;/code&gt;. This is a necessary step for Store API to pop up a purchase dialog in desktop apps (non-UWP); otherwise, the purchase window has no owner and behaves abnormally—a window without belonging is like a person without belonging: always drifting.&lt;/p&gt;

&lt;p&gt;The renderer process calls the main process through the bridge exposed by &lt;code&gt;preload&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscriptionBridge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubscriptionBridge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptionChannels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;verifyStartup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptionChannels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifyStartup&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptionChannels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptionChannels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;onDidChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptionChannels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptionChannels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State changes are pushed to all windows via &lt;code&gt;broadcastSnapshotChanged&lt;/code&gt;. After purchase completion, &lt;code&gt;completePurchase&lt;/code&gt; triggers a &lt;code&gt;refresh('purchase')&lt;/code&gt;, and the new state is automatically broadcast, updating the subscription UI in the renderer process in real time.&lt;/p&gt;

&lt;p&gt;Additionally, &lt;code&gt;main.ts&lt;/code&gt; has a &lt;code&gt;setInterval&lt;/code&gt; silently syncing in the background (&lt;code&gt;subscriptionService?.refresh('scheduled')&lt;/code&gt;). This allows the running app to catch renewals and unsubscribes users quietly make in the Store client. The frequency naturally can't be too high (Store has rate limits); the code uses minute-level intervals—not too far, not too close, just right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Easy-to-Fall Pits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First, native addon thread safety.&lt;/strong&gt; After WinRT async operations complete, the callback isn't on the JavaScript thread. Calling &lt;code&gt;Napi&lt;/code&gt; APIs directly in the callback will crash. The addon uses &lt;code&gt;Napi::ThreadSafeFunction::BlockingCall&lt;/code&gt; to deliver results back to the JS thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threadsafeFunction_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BlockingCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;Napi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Env&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Napi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PurchaseCompletion&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;unique_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PurchaseCompletion&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ownedData&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ResolveOnJs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ownedData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BlockingCall&lt;/code&gt; blocks the WinRT callback thread until the JS thread finishes processing. In this mode, the callback thread cannot be the JS thread itself, otherwise you get deadlock. Fortunately, WinRT's &lt;code&gt;Completed&lt;/code&gt; callbacks typically run on STA or thread pools, which satisfies this condition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, COM initialization.&lt;/strong&gt; The Electron main thread might have already initialized COM. The addon wraps &lt;code&gt;winrt::init_apartment&lt;/code&gt; in try-catch, ignoring failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;winrt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;init_apartment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;winrt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;apartment_type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;single_threaded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Electron might have already initialized COM for this thread; ignore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without handling this, repeated initialization throws exceptions and addon loading fails. Some errors, when ignored, are actually correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, window handle precision.&lt;/strong&gt; &lt;code&gt;getNativeWindowHandle()&lt;/code&gt; returns a &lt;code&gt;Buffer&lt;/code&gt;, which might be 4 bytes (32-bit) or 8 bytes (64-bit). It's then formatted as a hex string starting with &lt;code&gt;0x&lt;/code&gt; in the addon, and the C++ side parses it back to &lt;code&gt;HWND&lt;/code&gt; using &lt;code&gt;std::stoull&lt;/code&gt;. Why use strings instead of passing numbers directly? Because JavaScript's number precision is only 53 bits, and 64-bit pointers would lose precision. This pit is hard to discover without stepping in it—like some things, you can't explain them without experiencing them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fourth, state isolation.&lt;/strong&gt; Subscription and perpetual license states must be stored separately. HagiCode's spec explicitly requires that TurboEngine's persistence not overwrite sponsor's state. The two snapshots are separated by different &lt;code&gt;productKey&lt;/code&gt;s (&lt;code&gt;subscription&lt;/code&gt; and &lt;code&gt;turboengine&lt;/code&gt;), avoiding one product's refresh overwriting another's cache. When everyone minds their own business, the world is at peace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fifth, must refresh after purchase.&lt;/strong&gt; After purchase completes, you must refresh once more to broadcast. &lt;code&gt;completePurchase&lt;/code&gt; triggers &lt;code&gt;refresh('purchase')&lt;/code&gt; for both &lt;code&gt;succeeded&lt;/code&gt; and &lt;code&gt;already-purchased&lt;/code&gt; cases, because Store's purchase result only tells you the transaction status, not the current license details. License status must be queried again—between promise and reality, there's always one confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This implementation has been running for a while and is relatively stable. What's most worth borrowing isn't any specific trick, but this layered approach: completely isolating "dealing with Store" in the broker and addon layers, with the upper layers handling only pure business semantics.&lt;/p&gt;

&lt;p&gt;A few core experiences to remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Touch WinRT only in the C++ addon; the addon only does "async to JSON"—no business semantics.&lt;/li&gt;
&lt;li&gt;Standardization and state machine are two layers; don't mix raw data and business state together.&lt;/li&gt;
&lt;li&gt;On network failure, preserve the last good state and mark it stale—don't take away paid users' permissions.&lt;/li&gt;
&lt;li&gt;Decouple entitlements and status; feature code only looks at the &lt;code&gt;entitlements&lt;/code&gt; array.&lt;/li&gt;
&lt;li&gt;Non-Store environments use a degraded broker—never let "unsupported" become a crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're also doing Store commercialization for Electron apps, I hope this layered approach helps you avoid a few pitfalls.&lt;/p&gt;

&lt;p&gt;The solution shared in this article is what we actually stumbled through and optimized while developing HagiCode. If you think it has some value, it means our engineering skills are decent—which makes HagiCode itself worth a second look...&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/uwp/api/windows.services.store" rel="noopener noreferrer"&gt;Windows.Services.Store namespace - WinRT Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.electronjs.org/docs/latest/api/browser-window#wingetnativewindowhandle" rel="noopener noreferrer"&gt;Electron getNativeWindowHandle Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/api/n-api.html#n_api_napi_create_threadsafe_function" rel="noopener noreferrer"&gt;Node-API ThreadSafeFunction Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;For "integrating Microsoft Store subscription and permanent licenses in Electron desktop apps," a more prudent approach is to first validate key configurations, dependency boundaries, and implementation paths, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;When goals, steps, and acceptance criteria are clear, such solutions usually proceed more smoothly into actual delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-16-electron-msstore-subscription-license%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-16-electron-msstore-subscription-license%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>electron</category>
      <category>microsoftstore</category>
      <category>winrt</category>
    </item>
    <item>
      <title>From Scratch: How to Integrate Reasonix CLI into the HagiCode System</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Tue, 09 Jun 2026 03:05:06 +0000</pubDate>
      <link>https://dev.to/newbe36524/from-scratch-how-to-integrate-reasonix-cli-into-the-hagicode-system-409c</link>
      <guid>https://dev.to/newbe36524/from-scratch-how-to-integrate-reasonix-cli-into-the-hagicode-system-409c</guid>
      <description>&lt;h1&gt;
  
  
  From Scratch: How to Integrate Reasonix CLI into the HagiCode System
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;This article shares the complete technical practice of integrating Reasonix CLI as a first-class Agent Provider into the HagiCode system, covering three-layer architecture design, key technical decisions, and frontend and backend implementation details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Reasonix CLI, as it happens, is a pretty interesting thing. It's an AI code assistant tool based on ACP (Agent Communication Protocol), providing powerful streaming and session management capabilities. Actually, in the HagiCode.Libs layer, we've already completed its underlying implementation. It's just that these components are still in an isolated state, like beautiful pearls that haven't been strung into a necklace. Users cannot use it through Hero profession selection, session execution paths, or monitoring panels, which is somewhat regrettable.&lt;/p&gt;

&lt;p&gt;The problem we face is: how to elevate Reasonix to the same level as Codex, Hermes, and other first-class Agent Providers, implementing complete backend routing and frontend display? This isn't simply a matter of registering an enum value. It requires building a complete chain from low-level abstraction to user interface. It's like building a house—you can't just lay a foundation and call it done. You have to build the walls and put up the roof.&lt;/p&gt;

&lt;p&gt;The challenge of this integration lies in the fact that Reasonix, as a local CLI tool, has its own personality and temperament. For example, it doesn't need a connection string—all parameters are configured by the user at runtime; it might not even be installed, requiring graceful degradation; it's compatible with anthropic series models, but also has its own ACP-specific parameters like effort, budget, and so on. It's like a person with their own unique way of handling things—you can't force it.&lt;/p&gt;

&lt;p&gt;After careful architectural design and multiple rounds of discussion, we finally adopted a clear three-layer architecture solution, successfully integrating Reasonix into the system. This solution not only solved the immediate problem but also provided a reusable pattern for subsequent similar CLI Provider integrations. Actually, many things are like this—once you find the right method, the path forward becomes much easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an open-source AI code assistant project dedicated to providing developers with powerful code generation, refactoring, and optimization capabilities. During development, we encountered various technical challenges, and integrating Reasonix as a first-class Agent Provider was one of them. If you find the solution shared in this article valuable, it means our engineering practice isn't bad, and HagiCode itself is worth paying attention to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Content
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Technical Architecture Design
&lt;/h3&gt;

&lt;p&gt;The system uses a clear three-layer architecture to separate concerns, with each layer having clear responsibility boundaries:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HagiCode.Libs layer&lt;/strong&gt;: This layer is already complete, providing the abstraction and specific implementation of the CLI provider. It defines the &lt;code&gt;ICliProvider&amp;lt;ReasonixOptions&amp;gt;&lt;/code&gt; interface, implements &lt;code&gt;ReasonixProvider&lt;/code&gt; to handle ACP streaming and session management, and supports parameters like effort, budget, yolo, transcript, and so on. The responsibility of this layer is to provide stable, reusable low-level capabilities without involving any business logic. It's like the foundation of a house—though invisible, it's very important.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;hagicode-core layer&lt;/strong&gt;: This is the focus of our integration. It's responsible for bridging the low-level abstraction to the system's unified interface. Specific work includes registering the &lt;code&gt;AIProviderType.ReasonixCli = 12&lt;/code&gt; enum value, creating &lt;code&gt;ReasonixCliProvider&lt;/code&gt; as a thin adapter to bridge the Libs layer, implementing &lt;code&gt;ReasonixGrain&lt;/code&gt; to handle session state and execution flow, and integrating the Hero system for parameter mapping and configuration management. The core of this layer is coordinating components to build a complete business chain. It's like the load-bearing walls of a house, connecting all parts together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;web layer&lt;/strong&gt;: Responsible for displaying and collecting configurations from users. We need to regenerate OpenAPI types to support the new enum value, implement visual type mapping to give Reasonix its own icon and display name, create a CLI parameter configuration form allowing users to configure various parameters, and add multi-language support. The focus of this layer is user experience and interaction design. It's like the decoration of a house—whether it's done well directly affects how comfortable it is to live in.&lt;/p&gt;

&lt;p&gt;Such layered design allows each layer to focus on its own responsibilities, reducing system complexity and facilitating subsequent maintenance and expansion. Actually, many times, clarifying things makes them simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Technical Decisions
&lt;/h3&gt;

&lt;p&gt;During implementation, we made several key technical decisions that had important impacts on the final architecture and user experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 1: Use a Dedicated Grain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We created an independent &lt;code&gt;ReasonixGrain : IReasonixGrain, IExecutorStreamGrain&lt;/code&gt; rather than trying to reuse some shared Grain. This decision follows the established pattern of the system's existing 11 providers. While there might be some code duplication, a dedicated Grain allows us to have fine-grained control over Reasonix's specific features, such as its unique session binding mechanism and ACP message mapping. We also defined an empty response DTO &lt;code&gt;ReasonixResponse&lt;/code&gt; as a type discriminator. Although it doesn't contain actual data, it plays an important role in the type system. It's like everyone having their own room—even if empty, it's their own space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 2: Don't Create a Dedicated Settings Class&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike some Providers that need connection strings, all Reasonix configurations are set by the user at runtime and don't require startup validation. Therefore, we didn't create a dedicated Settings class. Instead, we store all configurations in the &lt;code&gt;AIProviderOptions.Providers[ReasonixCli].Settings&lt;/code&gt; dictionary. This pattern is consistent with other local CLI Providers like Qoder, Kiro, and Kimi, simplifying code structure and avoiding unnecessary abstraction layers. Supported setting keys include: &lt;code&gt;effort&lt;/code&gt;, &lt;code&gt;budgetUsd&lt;/code&gt;, &lt;code&gt;transcriptPath&lt;/code&gt;, &lt;code&gt;enableYolo&lt;/code&gt;, &lt;code&gt;arguments&lt;/code&gt;, &lt;code&gt;startupTimeoutMs&lt;/code&gt;, &lt;code&gt;reasoning&lt;/code&gt;. Sometimes simpler is better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 3: Provider Strategy Health Monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reasonix is a CLI installed locally by the user—it might not be installed at all, or might not be in the system PATH. In such cases, we shouldn't directly error out but should handle it gracefully with degradation. We use the &lt;code&gt;Provider&lt;/code&gt; strategy to check if the CLI is available through &lt;code&gt;CommandUtil.TryResolveExecutablePath&lt;/code&gt;. If the check fails, the UI displays as "unavailable" but doesn't affect other parts of the system. This design makes the system more robust and provides clear feedback to users. After all, no one wants the entire system to crash because of a minor issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 4: Economic System Classification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the HagiCode system, different Providers have different economic system classifications. We decided to let Reasonix use the &lt;code&gt;'claude'&lt;/code&gt; economic system classification by default, since Reasonix itself is compatible with the anthropic series of models. Currently, only Codex and Copilot have dedicated economic system classifications, while other Providers reuse existing classifications. This maintains system simplicity while correctly handling billing and cost statistics. Reusing is also a kind of wisdom—not everything needs to start from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 5: Model Compatibility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reasonix supports multiple models through the &lt;code&gt;--model&lt;/code&gt; flag, especially the anthropic series. We added compatibility mapping in &lt;code&gt;secondary-professions.index.json&lt;/code&gt;, allowing users to select these models in Reasonix. This design respects Reasonix's capabilities while maintaining system consistency. Users don't need to understand the underlying differences to smoothly use various models. Users have it hard enough—let's keep things simple for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend Implementation Details
&lt;/h3&gt;

&lt;p&gt;Backend implementation is divided into several key parts, each with its own unique technical points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enum and Type Registration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, we need to register the new Provider type in the system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AIProviderType.cs&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other providers&lt;/span&gt;
    &lt;span class="n"&gt;ReasonixCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// AIProviderTypeExtensions.cs&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_typeMap&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other mappings&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Reasonix"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"reasonix"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"reasonix-cli"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ReasonixCli"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enum value needs to coordinate with other concurrent changes to avoid conflicts. We chose the value 12 because it's the next available number. It's like lining up—there has to be some order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thin Adapter Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReasonixCliProvider&lt;/code&gt; is the key component connecting the Libs layer and the system's unified interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReasonixCliProvider&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAIProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IVersionedAIProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAsyncDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SupportedSettingKeys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s"&gt;"effort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"budgetUsd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"transcriptPath"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"enableYolo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"startupTimeoutMs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"reasoning"&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ICliProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReasonixOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_provider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_sessionBindings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AIStreamingChunk&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;StreamCoreAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;AIRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnumeratorCancellation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;MapToStreamingChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;ReasonixOptions&lt;/span&gt; &lt;span class="nf"&gt;BuildOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AIRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ReasonixOptions&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ExecutablePath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetExecutablePath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;WorkingDirectory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetWorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Effort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"effort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Budget&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"budgetUsd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Yolo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"enableYolo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;TranscriptPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"transcriptPath"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Arguments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;StartupTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetStartupTimeout&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;EnvironmentVariables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_environmentVariables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;CessionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;GetCessionId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key responsibilities of this adapter are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate configuration parameters and reject unsupported setting keys&lt;/li&gt;
&lt;li&gt;Maintain session binding relationships, supporting session resumption&lt;/li&gt;
&lt;li&gt;Map ACP messages to the system's unified format&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;ProviderErrorAutoRetryCoordinator&lt;/code&gt; to implement automatic retry&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's like a translator, translating one language to another while ensuring the meaning is accurate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orleans Grain Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReasonixGrain&lt;/code&gt; is responsible for handling session state and execution flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReasonixGrain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Grain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IReasonixGrain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IExecutorStreamGrain&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ExecutorToolLifecycleStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_toolLifecycleState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReasonixResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExecuteCommandStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;executionMessageId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;systemMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;requestSettings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isEdit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;executionMessageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;systemMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestSettings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReasonixResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;AIRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnumeratorCancellation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_cancellationTokenSource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;linkedToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateLinkedTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_cancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ResolveReasonixProviderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;linkedToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildChunkResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAIProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ResolveReasonixProviderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Hero-aware configuration fallback logic&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;HeroProviderResolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResolveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_aiProviderFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core functions of the Grain include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;[PersistentState("reasonix-interop")]&lt;/code&gt; to maintain session state&lt;/li&gt;
&lt;li&gt;Implement Hero-aware configuration fallback logic&lt;/li&gt;
&lt;li&gt;Track tool lifecycle status&lt;/li&gt;
&lt;li&gt;Support cancellation token chains, ensuring execution can be interrupted in a timely manner&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's like a housekeeper, arranging things in an orderly manner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hero System Integration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Hero system is HagiCode's profession configuration system, and we need to integrate Reasonix into this system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// HeroAppService.cs&lt;/span&gt;
&lt;span class="c1"&gt;// Family inference&lt;/span&gt;
&lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"reasonix"&lt;/span&gt;

&lt;span class="c1"&gt;// Managed CLI parameters&lt;/span&gt;
&lt;span class="n"&gt;ManagedCliParameterKeysByProvider&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"binary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"effort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"budgetUsd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"transcriptPath"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"enableYolo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"startupTimeoutMs"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Managed model parameters&lt;/span&gt;
&lt;span class="n"&gt;ManagedModelParameterKeysByProvider&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the profession directory configuration file &lt;code&gt;main-professions.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profession-reasonix"&lt;/span&gt;
  &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reasonix"&lt;/span&gt;
  &lt;span class="na"&gt;Family&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasonix"&lt;/span&gt;
  &lt;span class="na"&gt;Summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hero.professionCopy.primary.reasonix.summary"&lt;/span&gt;
  &lt;span class="na"&gt;Icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executor-avatar:Reasonix"&lt;/span&gt;
  &lt;span class="na"&gt;SourceLabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hero.professionCopy.sources.aiProvidersReasonixCli"&lt;/span&gt;
  &lt;span class="na"&gt;ProviderType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ReasonixCli"&lt;/span&gt;
  &lt;span class="na"&gt;SortOrder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;130&lt;/span&gt;
  &lt;span class="na"&gt;DefaultEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;DefaultParameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;binary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasonix"&lt;/span&gt;
    &lt;span class="na"&gt;effort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium"&lt;/span&gt;
    &lt;span class="na"&gt;enableYolo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;
    &lt;span class="na"&gt;startupTimeoutMs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this configuration, users can see the Reasonix option in the Hero configuration interface and make personalized settings. It's like registering someone's household—with an identity, they can live normally in this society.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend Implementation Details
&lt;/h3&gt;

&lt;p&gt;Frontend implementation is mainly responsible for user interaction and display, also divided into several key parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type Generation and Visual Mapping&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, we need to regenerate OpenAPI types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run generate:api:once
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates type definitions containing &lt;code&gt;REASONIX_CLI = 'ReasonixCli'&lt;/code&gt;. Then in visual mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// executorTypeAdapter.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ExecutorVisualType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Claude&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Codex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Copilot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reasonix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolveExecutorVisualTypeFromProviderType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;providerType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PCode_Models_AIProviderType&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ExecutorVisualType&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other cases&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;PCode_Models_AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REASONIX_CLI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reasonix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way Reasonix has its own visual type and can display corresponding icons and styles. It's like everyone having their own ID photo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration Form Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;HeroCliEquipmentForm.tsx&lt;/code&gt;, we added a dedicated configuration form for Reasonix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;PCode_Models_AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REASONIX_CLI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;binary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Form.Item&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;effort&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Select&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Option&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;None&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Select.Option&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Option&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Low&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Select.Option&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Option&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Medium&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Select.Option&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Option&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;High&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Select.Option&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Select&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Form.Item&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;budgetUsd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InputNumber&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Form.Item&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transcriptPath&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Form.Item&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enableYolo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Switch&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Form.Item&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arguments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Form.Item&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;startupTimeoutMs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InputNumber&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Form.Item&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This form covers all parameters supported by Reasonix, allowing users to configure according to their needs. It's like tailoring clothes for someone—fit is most important.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-language Support&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To enable international users to use it as well, we added multi-language support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# locales/*/common/hero.yml&lt;/span&gt;
&lt;span class="na"&gt;profession&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;reasonix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reasonix"&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;based&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ACP"&lt;/span&gt;
      &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;effort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Computational&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;effort"&lt;/span&gt;
        &lt;span class="na"&gt;budgetUsd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Budget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(USD)"&lt;/span&gt;
        &lt;span class="na"&gt;transcriptPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transcript&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;path"&lt;/span&gt;
        &lt;span class="na"&gt;enableYolo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Enable&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;YOLO&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mode"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After all, if the language doesn't work, even good things can't be used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health Monitoring Mapping&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The frontend also needs to display Reasonix's health status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// healthApi.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MONITORING_CHANNEL_FALLBACKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other providers&lt;/span&gt;
  &lt;span class="na"&gt;reasonix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reasonix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;executor-avatar:Reasonix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapProviderTypeToMonitoringCliId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;providerType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PCode_Models_AIProviderType&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other cases&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;PCode_Models_AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REASONIX_CLI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reasonix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way users can see Reasonix's status in the monitoring panel. If the CLI is not installed or unavailable, they will receive clear prompts. It's like a doctor examining a patient—catching problems early leads to early treatment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Practices and Considerations
&lt;/h3&gt;

&lt;p&gt;During implementation, we summarized some best practices and points to note.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parameter Validation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ReasonixCliProvider must strictly validate configuration parameters and reject unsupported setting keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ValidateConfigurationOverrides&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;SupportedSettingKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HeroProviderConfigurationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;$"Unsupported setting key '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;' for Reasonix provider"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents users from configuring wrong parameters and avoids runtime errors. It's like a goalkeeper—can't let things that shouldn't enter get in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session Binding Management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;ConcurrentDictionary&lt;/code&gt; to manage session bindings, supporting session resumption:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_sessionBindings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cessionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Bind existing sessions in subsequent requests&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sessionBindings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;boundSessionId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boundSessionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design allows users to resume previous sessions after interruption, providing a better experience. It's like remembering a story so you can continue telling it next time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful Degradation Handling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The frontend should check CLI availability and provide friendly prompts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reasonixAvailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;healthApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkCliAvailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reasonix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;reasonixAvailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reasonix CLI is not installed, please configure it in the system PATH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't let users encounter inexplicable errors—check in advance and provide clear guidance. After all, no one wants to get stuck inexplicably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test Coverage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Comprehensive testing is key to quality assurance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteCommandStreamAsync_WithValidCommand_StreamsReasonixResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Arrange&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;grain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_grainFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IReasonixGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"test-cession"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReasonixResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Act&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteCommandStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"help"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Assert&lt;/span&gt;
    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;NotBeEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Kind&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ExecutorResponseKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;BeTrue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Such unit tests can verify that core functions work correctly, preventing regression errors. It's like doing practice questions before an exam—you need to ensure you've actually mastered it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Through this Reasonix integration practice, we successfully elevated a local CLI tool to the system's first-class Agent Provider. Throughout the process, we followed established architecture patterns, made reasonable technical decisions, and ultimately implemented a complete, well-integrated solution with good user experience.&lt;/p&gt;

&lt;p&gt;The core value of this solution lies in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clear three-layer architecture separates concerns and reduces complexity&lt;/li&gt;
&lt;li&gt;Dedicated Grain and thin adapter design maintains flexibility&lt;/li&gt;
&lt;li&gt;Graceful degradation and health monitoring improve user experience&lt;/li&gt;
&lt;li&gt;Comprehensive parameter validation and session management ensure reliability&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For other similar CLI Provider integrations, this solution provides a reusable pattern. We hope this practice can help other developers, and we welcome everyone to exchange experiences in the HagiCode project.&lt;/p&gt;

&lt;p&gt;Actually, many things are like this—at first they seem difficult, but as long as you find the right method and take it step by step, you can always solve it. It's like climbing a mountain—the peak looks far away, but as long as you don't give up, you can always climb up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Returning to the theme "From Scratch: How to Integrate Reasonix CLI into the HagiCode System," what's truly worth repeatedly confirming isn't scattered techniques, but whether constraint conditions, implementation boundaries, and engineering trade-offs have been clearly understood.&lt;/p&gt;

&lt;p&gt;As long as the judgment bases in the article are distilled into stable checklist items, you can make reliable decisions more quickly when facing similar problems in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-09-integrating-reasonix-cli-into-hagicode%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-09-integrating-reasonix-cli-into-hagicode%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hagicode</category>
      <category>orleans</category>
      <category>react</category>
    </item>
    <item>
      <title>Practical Journey of Integrating Pi Agent into HagiCode</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Mon, 08 Jun 2026 12:20:30 +0000</pubDate>
      <link>https://dev.to/newbe36524/practical-journey-of-integrating-pi-agent-into-hagicode-58l0</link>
      <guid>https://dev.to/newbe36524/practical-journey-of-integrating-pi-agent-into-hagicode-58l0</guid>
      <description>&lt;h1&gt;
  
  
  Practical Journey of Integrating Pi Agent into HagiCode
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;This article shares our practical experience in integrating Pi agent as a first-class workflow entry point in the HagiCode project, including architecture design, core component implementation, and frontend and backend adaptation details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Actually, everything started with that question: in the HagiCode Mono project, although &lt;code&gt;repos/Hagicode.Libs&lt;/code&gt; already implemented the reusable &lt;code&gt;PiProvider&lt;/code&gt;, &lt;code&gt;repos/hagicode-core&lt;/code&gt; and &lt;code&gt;repos/web&lt;/code&gt; had not yet elevated Pi to a project-level first-class Agent CLI. This is like having a good pair of shoes but not tying the laces properly—you can walk, but it always feels like something is missing.&lt;/p&gt;

&lt;p&gt;The existing &lt;code&gt;PiProvider&lt;/code&gt; is located at &lt;code&gt;HagiCode.Libs/src/HagiCode.Libs.Providers/Pi/PiProvider.cs&lt;/code&gt;. It implements a CLI communication protocol based on line-delimited JSON (&lt;code&gt;--mode json --print&lt;/code&gt;), supporting session management, tool calls, and streaming output. This code is well-written, but it's just lying there, not yet awakened.&lt;/p&gt;

&lt;p&gt;This situation created a gap: Pi's underlying capabilities were complete, but the project-level integration chain (from user configuration to execution monitoring) was missing. It's like painting a good picture but not hanging it on the wall for people to appreciate. Therefore, we needed to integrate Pi as a new active provider &lt;code&gt;PiCli&lt;/code&gt; into the entire system, making it a first-class workflow entry point like other Agent CLIs.&lt;/p&gt;

&lt;p&gt;After all, what we wanted was a complete experience, not scattered fragments.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an intelligent code assistant project, and during development we needed to integrate multiple AI capability providers. The Pi agent integration solution described in this article is a reusable integration pattern summarized from our practice, which has reference value for similar multi-provider integration scenarios. The project's GitHub address is &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Design
&lt;/h2&gt;

&lt;p&gt;Pi agent integration adopted the &lt;strong&gt;thin adapter pattern&lt;/strong&gt;, rather than re-implementing the Pi process protocol at the core layer. This design decision is not complex—we just thought, why reinvent the wheel?&lt;/p&gt;

&lt;p&gt;After all:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Avoid duplicate implementation&lt;/strong&gt;: The parameter building, process launching, JSON parsing, and error handling logic in &lt;code&gt;PiProvider&lt;/code&gt; are already complete. Re-implementing would create two sources of behavior and two test matrices. That's fine, but maintenance would be troublesome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintain consistency&lt;/strong&gt;: Consistent with the integration methods of existing CLIs like Kimi, Gemini, Reasonix, and DeepAgents—all delegate through thin adapters to the libs layer implementation. Everyone does this, just follow along.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of concerns&lt;/strong&gt;: &lt;code&gt;hagicode-core&lt;/code&gt; focuses on runtime contracts and business logic, while process details are left to &lt;code&gt;HagiCode.Libs&lt;/code&gt;. Each does their own job, isn't that beautiful?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This design allows HagiCode to quickly integrate new AI capability providers while keeping the core layer clean. After all, simplicity is beauty, and efficiency is money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Component Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Provider Enumeration and Factory
&lt;/h3&gt;

&lt;p&gt;Added &lt;code&gt;PiCli = 13&lt;/code&gt; enumeration value in &lt;code&gt;hagicode-core/src/PCode.Models/AIProviderType.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ClaudeCodeCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other providers&lt;/span&gt;
    &lt;span class="n"&gt;PiCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enumeration value is the root of the provider identity, affecting the OpenAPI-generated &lt;code&gt;PCode_Models_AIProviderType.ts&lt;/code&gt; frontend enumeration, the creation branch of &lt;code&gt;AIProviderFactory&lt;/code&gt;, and Provider parsing and active provider judgment logic. Register the new creation branch in &lt;code&gt;AIProviderFactory&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PiCli&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PiCliProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;providerFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICliProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PiOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;providerFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAgentCliRuntimeEnvironmentResolver&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actually, this code is nothing special, just an enumeration plus a switch case. But they are important, like notes on a musical score—adding them one by one creates a complete melody.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thin Adapter Implementation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PiCliProvider.cs&lt;/code&gt; is the core thin adapter, implementing the &lt;code&gt;IAIProvider&lt;/code&gt;, &lt;code&gt;IVersionedAIProvider&lt;/code&gt;, and &lt;code&gt;IAsyncDisposable&lt;/code&gt; interfaces. It receives &lt;code&gt;ICliProvider&amp;lt;PiOptions&amp;gt;&lt;/code&gt; (from &lt;code&gt;HagiCode.Libs&lt;/code&gt;) through the constructor, maps &lt;code&gt;AIRequest&lt;/code&gt; / &lt;code&gt;ProviderConfiguration&lt;/code&gt; to &lt;code&gt;PiOptions&lt;/code&gt;, and then delegates operations like execution, ping, and version query to the underlying provider.&lt;/p&gt;

&lt;p&gt;The key is to handle Pi's unique JSON event stream, including &lt;code&gt;assistant.thought&lt;/code&gt;, &lt;code&gt;assistant&lt;/code&gt;, &lt;code&gt;terminal.completed&lt;/code&gt;, and other events. These events need to be correctly parsed and converted to the system's standard format during streaming output. It's a bit like translation—conveying meaning from one language to another.&lt;/p&gt;

&lt;h3&gt;
  
  
  Main Profession Preset
&lt;/h3&gt;

&lt;p&gt;Added &lt;code&gt;profession-pi&lt;/code&gt; main profession entry in &lt;code&gt;main-professions.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profession-pi"&lt;/span&gt;
  &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pi"&lt;/span&gt;
  &lt;span class="na"&gt;Family&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pi"&lt;/span&gt;
  &lt;span class="na"&gt;Summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hero.professionCopy.primary.pi.summary"&lt;/span&gt;
  &lt;span class="na"&gt;Icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executor-avatar:Pi"&lt;/span&gt;
  &lt;span class="na"&gt;SourceLabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hero.professionCopy.sources.aiProvidersPiCli"&lt;/span&gt;
  &lt;span class="na"&gt;ProviderType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PiCli"&lt;/span&gt;
  &lt;span class="na"&gt;SortOrder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;59&lt;/span&gt;
  &lt;span class="na"&gt;DefaultEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;DefaultParameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;binary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pi"&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omniroute"&lt;/span&gt;
    &lt;span class="na"&gt;thinking&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;balanced"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that the backend snapshot and frontend fallback directory have consistent Pi identity, allowing Pi configuration to be managed through the existing Hero editor, and Pi's addition won't break the consumability of existing main profession entries. After all, we don't want to break existing things just because we added a new feature—that would be penny-wise and pound-foolish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring Registry
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;AgentCliMonitoringRegistry&lt;/code&gt; needs to add Pi's monitoring descriptor so the system can parse executable paths, display brand names, perform health probes, and display Pi status in the status bar and health details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AgentCliMonitoringDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CliId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"pi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Pi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProviderType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PiCli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DisplayOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentCliMonitoringStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Grain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NotConfiguredMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Pi CLI is not configured or executable not found."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EnabledPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="n"&gt;ExecutablePathConfigPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Hero:PrimaryProfessions:Pi:ExecutablePath"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;DefaultExecutablePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"pi"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The monitoring system is like a dashboard, telling you how the car is running. Pi's status is clear at a glance, so users can know if everything is normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend Adaptation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Executor Type Adaptation
&lt;/h3&gt;

&lt;p&gt;The frontend needs to update &lt;code&gt;executorTypeAdapter.ts&lt;/code&gt;, adding Pi's type recognition logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PI_IDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;PCode_Models_AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI_CLI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PiCli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;picli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pi-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isPi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalizedLower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PI_IDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;normalizedLower&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pi-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;normalizedLower&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;picli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;normalizedLower&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is like giving Pi several names—no matter what you call it, it knows you're referring to it. After all, what you call it doesn't matter, what matters is knowing who it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallback Hero Directory
&lt;/h3&gt;

&lt;p&gt;Add Pi's fallback entry in &lt;code&gt;hero.ts&lt;/code&gt; to ensure that even if backend data is not loaded, the frontend can still display Pi configuration normally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profession-pi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hero.professionCopy.primary.pi.summary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;executor-avatar:Pi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sourceLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hero.professionCopy.sources.aiProvidersPiCli&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;providerType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI_CLI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sortOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isReadOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;managedParameterKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// parameter keys supported in the first version&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;defaultParameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;omniroute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;thinking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;balanced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fallback is like a backup plan—just in case the backend goes down or data doesn't load, the frontend can still work normally. After all, no one wants to be in a panic when that happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Localization Copy and Forms
&lt;/h3&gt;

&lt;p&gt;Add Pi-related translations in &lt;code&gt;locales/*/common/{hero,settings}.yml&lt;/code&gt;, and add a configuration field block for Pi in &lt;code&gt;HeroCliEquipmentForm.tsx&lt;/code&gt;, supporting binary, provider, thinking, sessionDirectory, and tool/session switch fields.&lt;/p&gt;

&lt;p&gt;The first version of Pi only exposes the minimum necessary fields. Complex features like tool allowlist/denylist and environment variable editors are explicitly deferred to subsequent changes. After all, you can't become fat in one bite, take it slow and go faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend Configuration
&lt;/h3&gt;

&lt;p&gt;Configure Pi parameters in &lt;code&gt;appsettings.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Hero&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;PrimaryProfessions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Pi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ExecutablePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/bin/pi&lt;/span&gt;
      &lt;span class="na"&gt;Provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;omniroute&lt;/span&gt;
      &lt;span class="na"&gt;Thinking&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;balanced&lt;/span&gt;
      &lt;span class="na"&gt;SessionDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.pi/sessions&lt;/span&gt;
      &lt;span class="na"&gt;NoSession&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;DisableAllTools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;DisableBuiltinTools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Frontend Configuration
&lt;/h3&gt;

&lt;p&gt;Configure Pi profession in the Hero editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-pi-profession&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Pi Profession&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;providerType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI_CLI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;primaryModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PiCli&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;glm-4.7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;providerSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;omniroute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;thinking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;balanced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;sessionDirectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/Users/username/.pi/sessions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;noSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;disableAllTools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;disableBuiltinTools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration files are like recipes—follow them and you'll make good food. But sometimes, even following the recipe, you can still burn the food... though this configuration is quite clear, so there shouldn't be any problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation and Testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend Validation
&lt;/h3&gt;

&lt;p&gt;Validate main profession presets in tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;presetProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSnapshotAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;piProfession&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"profession-pi"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;piProfession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldNotBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;piProfession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PiCli&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;piProfession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Family&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pi"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also need to verify Pi availability and health check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check Pi executable&lt;/span&gt;
which pi
pi &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;span class="c"&gt;# Verify backend provider registration&lt;/span&gt;
curl http://localhost:35168/api/health/agent-cli/pi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing is like exams—passing means you really know it. But sometimes, passing an exam doesn't mean you really understand it, but at least it shows you can get the answers right.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend Validation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Verify type parsing and display name&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolveExecutorVisualType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pi-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolveExecutorVisualType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PCode_Models_AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI_CLI&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolveExecutorDisplayName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PiCli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Verify fallback directory&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;piFallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findFallbackProfessionById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profession-pi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;piFallback&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;providerType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI_CLI&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These test cases cover the main logic paths, ensuring Pi can be correctly identified and displayed. After all, things shown to users can't have errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pi Executable Not Found
&lt;/h3&gt;

&lt;p&gt;If the health check returns "Pi executable was not found.", you need to check if pi is in PATH, or confirm if the configured path is correct. The solution is to ensure &lt;code&gt;pi&lt;/code&gt; is installed and in PATH, or configure the correct &lt;code&gt;ExecutablePath&lt;/code&gt; in &lt;code&gt;appsettings.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is like not being able to find your house keys—you need to think if you put them in the wrong place. Actually, the solution is simple—either put the keys back in the original place, or get a new lock.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Fields Not Recognized
&lt;/h3&gt;

&lt;p&gt;If you get the error "PiCli runtime settings [...] are not supported" at startup, check if you're only using the configuration fields supported in the first version. Fields supported in the first version include: &lt;code&gt;provider&lt;/code&gt;, &lt;code&gt;thinking&lt;/code&gt;, &lt;code&gt;sessionDirectory&lt;/code&gt;, &lt;code&gt;noSession&lt;/code&gt;, &lt;code&gt;disableAllTools&lt;/code&gt;, &lt;code&gt;disableBuiltinTools&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes it's just being greedy—wanting too many features, but the system doesn't support them. Actually, the first version's features are already enough; you can't chew too much at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cannot Select Pi in Frontend
&lt;/h3&gt;

&lt;p&gt;If there's no Pi option in the Hero editor, check if you've run &lt;code&gt;npm run generate:api&lt;/code&gt; to regenerate the frontend enumeration, whether there's a &lt;code&gt;profession-pi&lt;/code&gt; entry in &lt;code&gt;hero.ts&lt;/code&gt;, and whether the localization copy is added correctly.&lt;/p&gt;

&lt;p&gt;Troubleshooting is like finding a lost item—you have to do it step by step. After all, blind searching won't find it; you need to search logically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use thin adapter pattern&lt;/strong&gt;: Don't re-implement the process protocol at the core layer; delegate to the libs layer provider. This avoids duplicate implementation and maintains code consistency. After all, reinventing the wheel is not only tiring but also prone to problems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Maintain naming consistency&lt;/strong&gt;: Use consistent naming conventions across frontend and backend to avoid confusion. Provider enumeration uses &lt;code&gt;PiCli&lt;/code&gt;, CLI ID uses &lt;code&gt;"pi"&lt;/code&gt;, display name uses &lt;code&gt;"Pi"&lt;/code&gt;. Good names mean lower communication costs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prioritize presets&lt;/strong&gt;: The first version should be based on the &lt;code&gt;profession-pi&lt;/code&gt; preset, rather than requiring users to manually configure. This allows users to get started quickly and reduces configuration complexity. Users like simple things; leave the complex ones to me.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Focus on error messages&lt;/strong&gt;: Ensure error messages are clear and actionable, helping users quickly locate problems. Clear error messages mean users won't panic over a single error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Version compatibility&lt;/strong&gt;: Consider the serialization stability of &lt;code&gt;AIProviderType&lt;/code&gt; enumeration values; changes need to be handled carefully. The &lt;code&gt;AIProviderType.PiCli = 13&lt;/code&gt; enumeration value cannot be easily modified. After all, changing this value might break backward compatibility—that would be troublesome.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Through the thin adapter pattern, we successfully integrated Pi agent into the HagiCode system, making it a first-class workflow entry point like other Agent CLIs. The core advantages of this solution are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoided duplicate implementation and reused the existing &lt;code&gt;PiProvider&lt;/code&gt; from the libs layer&lt;/li&gt;
&lt;li&gt;Maintained consistent integration methods with existing providers, reducing maintenance costs&lt;/li&gt;
&lt;li&gt;Implemented a complete chain from user configuration to execution monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HagiCode's practice proves that the thin adapter pattern is an effective solution for integrating AI capability providers. It enables us to quickly support new agents while maintaining system stability and maintainability.&lt;/p&gt;

&lt;p&gt;Actually, doing technology is like this—find a good pattern, then reuse it. This allows you to move forward quickly without losing direction. Like walking—once you find a good path, keep walking on it, just occasionally stop to enjoy the scenery...&lt;/p&gt;

&lt;p&gt;If you're also doing multi-provider AI capability integration, I hope this solution gives you some inspiration. If you're interested in the HagiCode project, welcome to exchange on GitHub. After all, technology makes progress through communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;HagiCode Installation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/earendil-works/pi-coding-agent" rel="noopener noreferrer"&gt;Pi Agent Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Around "Practical Journey of Integrating Pi Agent into HagiCode," a more robust approach is to first run through key configurations, dependency boundaries, and implementation paths step by step, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;When goals, steps, and acceptance criteria are clear, such solutions can more smoothly enter actual delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-08-integrating-pi-agent-into-hagicode%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-08-integrating-pi-agent-into-hagicode%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hagicode</category>
      <category>cli</category>
    </item>
    <item>
      <title>Automated Windows App Deployment to Microsoft Store</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Wed, 20 May 2026 10:18:56 +0000</pubDate>
      <link>https://dev.to/newbe36524/automated-windows-app-deployment-to-microsoft-store-21bd</link>
      <guid>https://dev.to/newbe36524/automated-windows-app-deployment-to-microsoft-store-21bd</guid>
      <description>&lt;h1&gt;
  
  
  Automated Windows App Deployment to Microsoft Store
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;How to achieve end-to-end automation from version parsing to Store release, saying goodbye to tedious manual operations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;If you have an Electron application you want to list on the Microsoft Store, you'll likely run into this problem: the Store doesn't support separate installation processes—you have to package the desktop application and server payload together into a complete AppX/MSIX package.&lt;/p&gt;

&lt;p&gt;That would be manageable enough. But the problem continues. Each time you release a new version, you need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check if desktop and server versions have been updated&lt;/li&gt;
&lt;li&gt;Check out code from the corresponding tags&lt;/li&gt;
&lt;li&gt;Download and inject the server payload&lt;/li&gt;
&lt;li&gt;Build the MSIX package&lt;/li&gt;
&lt;li&gt;Manually upload to the Microsoft Store&lt;/li&gt;
&lt;li&gt;Configure store information and pricing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If every step requires manual operation, that's too much hassle. And it's error-prone—you might not even remember which steps you've completed and which you haven't.&lt;/p&gt;

&lt;p&gt;Actually, this isn't really anyone's fault—manual operations are naturally prone to omissions. It's just that we really didn't want to go through this hassle every time, so we decided to solve this problem once and for all—by automating the entire process.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. As an AI code assistant, HagiCode provides both desktop and web clients and needs to support multiple distribution channels. In implementing automated Windows Store deployment, we've developed a complete automation solution.&lt;/p&gt;

&lt;p&gt;Speaking of which, this was probably an unexpected bonus. What started as a simple time-saver ended up becoming this comprehensive solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Architecture Analysis
&lt;/h2&gt;

&lt;p&gt;This problem actually involves coordinating multiple technical layers. We can break it down into five layers:&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Coordination Layer
&lt;/h3&gt;

&lt;p&gt;First, we need to know when a new version needs to be released. We need to parse the latest versions of desktop and server components from an Azure index (a blob storage we use to store build artifacts), then determine whether a new Store package needs to be generated.&lt;/p&gt;

&lt;p&gt;It's like asking yourself: Is it time to do this now?&lt;/p&gt;

&lt;h3&gt;
  
  
  Workspace Management Layer
&lt;/h3&gt;

&lt;p&gt;AppX builds depend on source code-level configuration and runtime layout, so we can't simply repackage existing build artifacts. We need to check out code from a specific tag in the desktop repository to ensure the correct source code state is used for the build.&lt;/p&gt;

&lt;p&gt;After all, if the source code is wrong, everything else is wasted effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime Packaging Layer
&lt;/h3&gt;

&lt;p&gt;This is the core part. We need to inject the server payload into the desktop application's packaging layout. This way, when the application starts, it will detect the packaged runtime and enter Steam mode (offline mode).&lt;/p&gt;

&lt;p&gt;If this step isn't done well, all previous efforts are wasted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build Output Layer
&lt;/h3&gt;

&lt;p&gt;Use electron-builder to generate an MSIX package that meets Store requirements. This step needs to run in a Windows environment because AppX builds require the Windows SDK.&lt;/p&gt;

&lt;p&gt;Some things just need to be done in the right place—won't work elsewhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distribution Layer
&lt;/h3&gt;

&lt;p&gt;Finally, publish to GitHub Releases and the Microsoft Store. GitHub Release serves as backup and version tracking, while the Microsoft Store is面向终端用户.&lt;/p&gt;

&lt;p&gt;Everything is ready, just needs this final push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall Architecture Design
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                    package-release workflow                  │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐    ┌─────────────────┐                │
│  │ resolve_plan    │───▶│   build         │                │
│  │ (Version parsing &amp;amp; skipping) │    │   (MSIX build)    │                │
│  └─────────────────┘    └────────┬────────┘                │
│         │                       │                          │
│         ▼                       ▼                          │
│  ┌─────────────────┐    ┌─────────────────┐                │
│  │ skip_summary    │    │   publish       │                │
│  │ (Skip report)   │    │   (Publish)     │                │
│  └─────────────────┘    └────────┬────────┘                │
│                                 │                          │
│                                 ▼                          │
│                        ┌─────────────────┐                 │
│                        │ publish_store   │                 │
│                        │ (Store publish) │                 │
│                        └─────────────────┘                 │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire process is driven by GitHub Actions and can be triggered on a schedule (every 4 hours) or manually. When manually triggered, you can specify specific desktop and server versions, or force a rebuild.&lt;/p&gt;

&lt;p&gt;Actually, once every four hours isn't that frequent—code updates vary in speed, but this approach is safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Build Plan Resolution
&lt;/h3&gt;

&lt;p&gt;The first step is to determine whether a new version needs to be built. We need to parse the current versions of desktop and server components, then check whether a corresponding release already exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/resolve-dispatch-build-plan.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveDispatchBuildPlan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eventPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;desktopAzureSasUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;serverAzureSasUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Parse trigger inputs&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeTriggerInputs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eventPayload&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Parse desktop and server versions from Azure index&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;desktopRelease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;serverRelease&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;resolveIndexRelease&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
      &lt;span class="na"&gt;azureSasUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;desktopAzureSasUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;platformId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win-x64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; 
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;resolveIndexRelease&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
      &lt;span class="na"&gt;azureSasUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;serverAzureSasUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;platformId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win-x64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; 
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// Generate release tag&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;desktopTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeGitTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;desktopRelease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;releaseTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deriveStoreReleaseTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;desktopRelease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;serverRelease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// For example: desktop-v1.2.3-server-v4.5.6&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if already exists (avoid duplicate builds)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingRelease&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findReleaseByTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;packerRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;releaseTag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shouldBuild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existingRelease&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forceRebuild&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;releaseTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingRelease&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shouldBuild&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;skipReason&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;desktopTag&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key in this step is the skip logic—if the same version combination has already been built, there's no need to run the entire build process again. This saves CI costs and time.&lt;/p&gt;

&lt;p&gt;Doing the same thing repeatedly probably doesn't make much sense. After all, time and resources are limited.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Workspace Preparation
&lt;/h3&gt;

&lt;p&gt;Once you've decided to build, you need to prepare a clean build environment. We use git worktree to check out code from a specific tag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/prepare-packaging-workspace.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;preparePackagingWorkspace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;planPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;platformId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;workspacePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;desktopSourcePath&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Use git worktree to check out desktop code from specific tag&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-C&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolvedDesktopSourcePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worktree&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--detach&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;desktopWorkspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`refs/tags/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// Validate workspace&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;validateDesktopWorkspace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;desktopWorkspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;storePackageConfig&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Create workspace manifest&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workspaceManifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;desktopWorkspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runtimeInjectionRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtimeRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;desktopTag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;desktopRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;workspaceManifest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantage of using worktree is that it doesn't affect the main working directory, builds can run in parallel, and it's easy to clean up after the build completes.&lt;/p&gt;

&lt;p&gt;After all, no one wants their main working directory messed up because of a build.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Server Payload Injection
&lt;/h3&gt;

&lt;p&gt;This step downloads and injects the server payload into the correct location.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/stage-server-payload.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stageServerPayload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;planPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;workspacePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;platformId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;azureSasUrl&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Download server payload from Azure index&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveAssetDownloadUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sasUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azureSasUrl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;downloadFromSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
    &lt;span class="na"&gt;sourceUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;assetSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="na"&gt;destinationPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;downloadPath&lt;/span&gt; 
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Extract and validate&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractArchive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;downloadPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extractionPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtimeRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resolveRuntimeRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extractionPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;validateServerPayloadRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runtimeRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platformId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Inject into packaging runtime layout&lt;/span&gt;
  &lt;span class="c1"&gt;// Target path is resources/portable-fixed/current&lt;/span&gt;
  &lt;span class="c1"&gt;// This path will be mapped to extra/portable-fixed/current in AppX&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;copyDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runtimeRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is the path mapping—&lt;code&gt;resources/portable-fixed/current&lt;/code&gt; gets packaged to &lt;code&gt;extra/portable-fixed/current&lt;/code&gt; in the AppX, so the application detects the local runtime at startup and enters offline mode.&lt;/p&gt;

&lt;p&gt;Paths are unforgiving—one wrong character and it won't be found.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. MSIX Build
&lt;/h3&gt;

&lt;p&gt;With the prepared workspace and server payload, you can build the MSIX package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/build-appx.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildAppx&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;planPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;workspacePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;platformId&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Generate Store-specific electron-builder config overlay&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;overlayConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeStoreElectronBuilderConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;desktopWorkspace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;workspaceManifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desktopWorkspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceConfigPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storePackageConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;electronBuilderConfigPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outputConfigPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron-builder.store.yml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Run desktop build command&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runShellCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;buildDesktopStoreCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;overlayConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;desktopScripts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;workspaceManifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desktopWorkspace&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Collect MSIX output&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storeOutputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findStoreOutputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pkgDirectory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;artifactPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;workspaceManifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;artifactFileName&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;copySingleFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;primaryOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;artifactPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The electron-builder configuration needs to include Store-specific metadata like package identity, publisher display name, etc. This information will be used during Store submission.&lt;/p&gt;

&lt;p&gt;If the configuration is wrong, the package won't build. Nothing more to say about that.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Publishing to GitHub
&lt;/h3&gt;

&lt;p&gt;After the build completes, you need to publish the artifacts to GitHub Releases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/publish-release.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;publishRelease&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;planPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;artifactsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;forceDryRun&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Build publication artifact manifest&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicationArtifacts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildPublicationArtifacts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;artifactsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;outputDir&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;dryRun&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create or update GitHub Release&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;releaseResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;upsertReleaseNotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Upload assets&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;publicationArtifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;uploadReleaseAsset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;releaseResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Release serves as version tracking and backup—even if the Store publication fails, the build artifacts are preserved.&lt;/p&gt;

&lt;p&gt;It's always good to have a backup plan. What if you need to roll back someday?&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Publishing to Microsoft Store
&lt;/h3&gt;

&lt;p&gt;The final step is publishing to the Store using the Microsoft Store CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/package-release.yml&lt;/span&gt;
&lt;span class="na"&gt;publish_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windows-latest&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure Microsoft Store CLI&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;microsoft/microsoft-store-apppublisher@v1.2&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish MSIX packages to Store&lt;/span&gt;
      &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pwsh&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;msstore reconfigure --tenantId $env:AZURE_AD_TENANT_ID ...&lt;/span&gt;
        &lt;span class="s"&gt;msstore publish "$($package.FullName)" -id $env:MICROSOFT_STORE_PRODUCT_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step needs to run on a Windows runner because the Microsoft Store CLI requires a Windows environment.&lt;/p&gt;

&lt;p&gt;Some things just need to be done in the right place—won't work in a different environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configuration Files
&lt;/h3&gt;

&lt;p&gt;The Store package configuration file defines package identity and build settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageIdentity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hagicode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisherDisplayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CN=8B6C8A94-AAE5-4C8B-9202-A29EA42B042F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"identityName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524.Hagicode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"backgroundColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transparent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"languages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zh-CN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zh-Hant"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"supportedWindowsTargets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"win-x64"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"desktop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"submodulePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inputs/hagicode-desktop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"electronBuilderConfigPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"electron-builder.yml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"buildScript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build:appx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"runtimeInjectionPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resources/portable-fixed/current"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration is painful to change, so it's best to get it right the first time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow Triggers
&lt;/h3&gt;

&lt;p&gt;Can be triggered on schedule or manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*/4&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;  &lt;span class="c1"&gt;# Run every 4 hours&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;desktop_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Desktop&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;selector'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;server_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Server&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;selector'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;force_rebuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Force&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rebuild&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;even&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;if&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exists'&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
      &lt;span class="na"&gt;dry_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;only,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;publish'&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automatic or manual, as long as it runs in the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Considerations
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Git Tag Requirements&lt;/strong&gt;: The desktop repository must contain tags corresponding to release versions (e.g., v1.2.3), otherwise the build will fail&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Azure SAS URL&lt;/strong&gt;: Need to configure &lt;code&gt;DESKTOP_AZURE_SAS_URL&lt;/code&gt; and &lt;code&gt;SERVER_AZURE_SAS_URL&lt;/code&gt; environment variables to access the index&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Microsoft Store Credentials&lt;/strong&gt;: Need to configure the following secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AZURE_AD_APPLICATION_CLIENT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_AD_APPLICATION_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_AD_TENANT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELLER_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MICROSOFT_STORE_PRODUCT_ID&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Runtime Validation&lt;/strong&gt;: Server payload must contain required runtime files, otherwise packaging will fail&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Windows Runner&lt;/strong&gt;: AppX builds and Store publishing need to run on Windows runners&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Duplicate Detection&lt;/strong&gt;: Scheduled runs check for existing release tags to avoid unnecessary builds&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We've probably stepped on all these pits, so writing them down as a reminder. After all, who wants to make the same mistakes twice?&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Windows Store automated deployment looks complex, but when broken down into independent steps, each step isn't difficult. The key is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use git worktree to manage build environments&lt;/li&gt;
&lt;li&gt;Inject server payload into the correct path&lt;/li&gt;
&lt;li&gt;Use electron-builder to generate MSIX packages&lt;/li&gt;
&lt;li&gt;Use Store CLI for final publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This solution has been running stably in the HagiCode project for several months, basically achieving the goal of "full automation"—except for initial credential and setup configuration, subsequent version releases require no manual intervention.&lt;/p&gt;

&lt;p&gt;Actually, many things are like this—they look difficult but are straightforward to do. It just takes some patience and experimentation.&lt;/p&gt;

&lt;p&gt;If you're working on similar automation, I hope this article provides some reference. Of course, every project is different and may need adjustments based on specific requirements.&lt;/p&gt;

&lt;p&gt;After all, all roads lead to Rome—some roads are just easier to travel than others...&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/AppPublisher" rel="noopener noreferrer"&gt;Microsoft Store CLI Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.electron.build/" rel="noopener noreferrer"&gt;electron-builder Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Website&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-05-20-windows-app-automation-to-microsoft-store%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-05-20-windows-app-automation-to-microsoft-store%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>windows</category>
      <category>microsoftstore</category>
      <category>electron</category>
      <category>cicd</category>
    </item>
    <item>
      <title>OpenCode Integration Practice: Architectural Evolution from Standalone Process to Shared Runtime</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Mon, 11 May 2026 03:54:48 +0000</pubDate>
      <link>https://dev.to/newbe36524/opencode-integration-practice-architectural-evolution-from-standalone-process-to-shared-runtime-3mj0</link>
      <guid>https://dev.to/newbe36524/opencode-integration-practice-architectural-evolution-from-standalone-process-to-shared-runtime-3mj0</guid>
      <description>&lt;h1&gt;
  
  
  OpenCode Integration Practice: Architectural Evolution from Standalone Process to Shared Runtime
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;This article shares the complete practice of HagiCode integrating the OpenCode AI assistant, including key design decisions during the architectural evolution process, pitfalls encountered, and final solutions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;OpenCode is an open-source AI coding assistant project hosted on GitHub. For a monorepo project like HagiCode, integrating OpenCode as a supported AI Provider means it can be used as a backend model for proposal generation, code editing, and workflow execution.&lt;/p&gt;

&lt;p&gt;However, this integration process didn't go as smoothly as imagined. Early on, there were two separate proposals: one planned to create a C# SDK, which was later abandoned—not really a loss; another for repository-level integration did persist. As OpenCode entered the formal session pipeline, we encountered a series of issues like session management and error recovery—after all, what must come will come.&lt;/p&gt;

&lt;p&gt;More troublesome was that the initially designed "standalone process per session" model exposed high resource overhead issues in actual operation, forcing a refactor to a "system-level shared runtime" model. We also stepped into the 400 BadRequest pit—reusing external endpoints lacking context causing request failures. It's all tears, really.&lt;/p&gt;

&lt;p&gt;This article is just organizing these pitfalls and design decisions to provide reference for projects that need to integrate OpenCode in the future. After all, beautiful things or people don't necessarily need to be possessed—as long as she remains beautiful, just watching her beauty quietly is enough... Technical sharing is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI-based code assistant project. During development, we needed to integrate multiple AI Providers, and OpenCode is one of them. The architectural evolution process shared below are all real experiences from our actual project—stepping in pits and optimizing them. No choice but to fill the pits we stepped in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overall Layered Design
&lt;/h3&gt;

&lt;p&gt;HagiCode's OpenCode integration architecture is divided into five layers, each with clear responsibilities:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Repository Integration Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Register the OpenCode repository through the MonoSpecs configuration system (&lt;code&gt;.hagicode/monospecs.yaml&lt;/code&gt;). There's a choice here: submodule or plain Git repository? We chose the latter, managing cloning and synchronization through a unified &lt;code&gt;scripts/clone-repos.mjs&lt;/code&gt; script. This is more flexible and avoids the permission and collaboration issues brought by submodules—after all, no one wants to see that error screen, but no choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Provider Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OpenCodeCliProvider&lt;/code&gt; implements the &lt;code&gt;IAIProvider&lt;/code&gt; interface, which is the standard abstraction layer for interfacing with external AI services. The initial proposal wanted "standalone process per session," but actual operation revealed resource overhead was too high, ultimately changing to shared runtime mode, managing system-level runtime lifecycle through &lt;code&gt;OpenCodeRuntimeCoordinator&lt;/code&gt;. It's nothing, really—the idea was beautiful, reality is cruel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Runtime Management Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OpenCodeRuntimeCoordinator&lt;/code&gt; is the core of the entire architecture, responsible for runtime startup, health checks, and失效重建. It uses &lt;code&gt;HagiCode.Libs.Providers.OpenCode&lt;/code&gt; as the HTTP client foundation, encapsulating all interactions with the OpenCode runtime. Like that winter night, the bamboo outside the window remained the same as yesterday, lacking the response to her—she still liked looking out the window—runtime is the same, needing someone to silently guard it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Session Persistence Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using SQLite database (&lt;code&gt;opencode-session-bindings-v2.db&lt;/code&gt;) to persist the mapping of CessionId to OpenCode SessionId. This design is critical, supporting session recovery and restart, avoiding creating new sessions each time. After all, memory—sometimes forgetting is better, but in the program world, having no memory really doesn't work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Error Recovery Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ProviderErrorAutoRetryCoordinator&lt;/code&gt; provides automatic retry mechanism,配合 &lt;code&gt;OpenCodeRetryableTerminalFailureClassifier&lt;/code&gt; to classify errors—which can be retried, which should fail directly. This layer greatly improves system robustness. Actually nothing much, just letting the system be like a person—fall down and get up again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Data Flow
&lt;/h3&gt;

&lt;p&gt;When an AI request comes in, the data flow goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request first reaches &lt;code&gt;OpenCodeCliProvider&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Provider requests runtime from &lt;code&gt;OpenCodeRuntimeCoordinator&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Coordinator checks if there's an available runtime, if not starts a new one&lt;/li&gt;
&lt;li&gt;Query or create session binding through CessionId&lt;/li&gt;
&lt;li&gt;Use bound SessionId to call OpenCode API&lt;/li&gt;
&lt;li&gt;If error occurs, decide whether to retry based on error type&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This process looks simple, but every step has had pitfalls. Does this have meaning? Perhaps, but we've stepped in them all... Also figured it out, stepping in pits is itself part of growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Design Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  From Standalone Process to Shared Runtime
&lt;/h3&gt;

&lt;p&gt;The initial &lt;code&gt;opencode-csharp-sdk&lt;/code&gt; proposal adopted a "standalone process per session" model. The idea was beautiful: good isolation, one process crash doesn't affect other sessions. But reality is cruel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High resource overhead: each process needs to load runtime, memory usage rises straight up&lt;/li&gt;
&lt;li&gt;Slow startup: frequent creation and destruction of processes, overhead can't be ignored&lt;/li&gt;
&lt;li&gt;Complex management: process lifecycle management is itself a troublesome matter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately we changed to "system-level shared runtime" mode. All sessions reuse the same runtime process, distinguishing different sessions through session id. This change reduced resource usage by an order of magnitude and significantly improved response speed. Actually nothing much, just changing "one person enjoying alone" to "everyone using together."&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Managed Endpoint vs External BaseUri
&lt;/h3&gt;

&lt;p&gt;Early on we encountered a weird 400 BadRequest problem. Investigation revealed it was because we reused an external BaseUrl but lacked necessary context information. OpenCode's runtime is stateful—directly using an external endpoint is equivalent to context loss—like a person without memory, at a loss.&lt;/p&gt;

&lt;p&gt;The solution is simple: maintain self-managed runtime, don't depend on external endpoints. Leave &lt;code&gt;BaseUri&lt;/code&gt; empty in configuration file, let the system manage runtime lifecycle itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;AI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;OpenCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;ExecutablePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opencode"&lt;/span&gt;
    &lt;span class="na"&gt;BaseUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;  &lt;span class="c1"&gt;# Leave empty, use self-managed runtime&lt;/span&gt;
    &lt;span class="na"&gt;Model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic/claude-sonnet-4-20250514"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration change looks inconspicuous, but solved the most headache-inducing problem at the time. After all, sometimes the answer is right before our eyes, we just took too many detours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session Binding Strategy
&lt;/h3&gt;

&lt;p&gt;Session binding is another key design. We use CessionId as binding key, supporting three modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;started&lt;/strong&gt;: New session, create new OpenCode SessionId&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;resumed&lt;/strong&gt;: Resume existing session, read binding from database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;restarted&lt;/strong&gt;: Restart session, create new SessionId but preserve history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This design makes session management very flexible—users can resume previous conversations at any time, and the system can automatically rebuild bindings after runtime restart. After all, memory—sometimes want to forget but can't, sometimes want to remember but can't... Memory in the program world is quite reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Plan
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Repository Integration
&lt;/h3&gt;

&lt;p&gt;Register OpenCode repository in &lt;code&gt;.hagicode/monospecs.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;repositories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/opencode"&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/anomalyco/opencode.git"&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OpenCode"&lt;/span&gt;
    &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⌨️"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the clone script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node scripts/clone-repos.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pulls the OpenCode source code locally, and it can be updated at any time later. Actually quite simple, as long as there are no errors...&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Provider Configuration
&lt;/h3&gt;

&lt;p&gt;Configure OpenCode provider in &lt;code&gt;appsettings.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;AI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;OpenCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;ExecutablePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opencode"&lt;/span&gt;
    &lt;span class="na"&gt;BaseUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="na"&gt;Model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic/claude-sonnet-4-20250514"&lt;/span&gt;
    &lt;span class="na"&gt;RequestTimeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
    &lt;span class="na"&gt;StartupTimeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several key parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RequestTimeoutSeconds&lt;/code&gt;: Timeout for single request, default 5 minutes—after all, waiting too long is quite torturous&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;StartupTimeoutSeconds&lt;/code&gt;: Runtime startup timeout, giving a full 1 minute&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Provider Restoration
&lt;/h3&gt;

&lt;p&gt;Bring OpenCode back into the AI Provider system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restore &lt;code&gt;OpenCodeCli&lt;/code&gt; in &lt;code&gt;AIProviderType&lt;/code&gt; enum&lt;/li&gt;
&lt;li&gt;Restore creation logic in &lt;code&gt;AIProviderFactory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecutorGrainFactory&lt;/code&gt; routes &lt;code&gt;OpenCodeCli&lt;/code&gt; to dedicated grain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These changes make OpenCode an equally-treated AI Provider, not a special case. Actually everyone is the same, nothing special or not special.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Runtime Management Code Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get runtime through OpenCodeRuntimeCoordinator&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_runtimeCoordinator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRuntimeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create or resume session&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ResolveSessionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Send prompt&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PromptAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;promptRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code looks very concise, but behind it does a lot of work: runtime startup, health checks, session binding query and creation. Like many things,表面上看不出什么，behind it all are stories.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Error Recovery Mechanism
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Detect retryable errors and rebuild runtime&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ShouldRetryWithFreshRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_runtimeCoordinator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvalidateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;recoveredRuntime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ResolveRuntimeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Retry with new runtime&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automatic retry mechanism greatly improves system robustness—network jitter, runtime occasional crashes can all automatically recover. Actually life is the same, fall down and get up, nothing big... Programs are much stronger than people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Configuration Quick Reference
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether to enable OpenCode provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ExecutablePath&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"opencode"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenCode executable path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BaseUri&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;External endpoint (recommended to leave empty)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Default model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RequestTimeoutSeconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;300&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Request timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StartupTimeoutSeconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;60&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runtime startup timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Session Binding Database Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;OpenCodeSessionBindings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BindingKey&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OpenCodeSessionId&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAtUtc&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UpdatedAtUtc&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bindings are retained for 30 days, automatically cleaned after expiration. This design both ensures session recovery capability and avoids unlimited data growth. After all, everything has an expiration, expired then clean up, it's also a form of letting go...&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Issues and Solutions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. 400 BadRequest Error&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check &lt;code&gt;BaseUri&lt;/code&gt; configuration, recommend leaving empty to use self-managed runtime. If must use external endpoint, ensure context is complete. Actually most times, the problem lies in "taking for granted."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Session Cannot Resume&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Confirm whether CessionId is correctly passed, check if corresponding binding record exists in database. Like searching for memory, there must be clues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Model Selection Issue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Supports two formats: &lt;code&gt;provider/model&lt;/code&gt; (like &lt;code&gt;anthropic/claude-sonnet-4&lt;/code&gt;) and no-provider format (like &lt;code&gt;claude-sonnet-4&lt;/code&gt;). All roads lead to Rome, just some roads are easier to walk, some roads slightly more winding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Tool Name Mismatch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tool names are automatically normalized, removing content after parentheses and colons. For example &lt;code&gt;read(path)&lt;/code&gt; becomes &lt;code&gt;read&lt;/code&gt;, pay attention when calling. These details aren't much, just easily overlooked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Auto Retry Not Working&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check if error classifier correctly identifies retryable errors. By default, network errors, runtime failures etc. automatically retry up to 3 times. After all, trying a few more times doesn't hurt, might just work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related Code Paths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Provider: &lt;code&gt;repos/hagicode-core/src/PCode.ClaudeHelper/AI/Providers/OpenCodeCliProvider.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Runtime Coordinator: &lt;code&gt;repos/hagicode-core/src/PCode.ClaudeHelper/AI/Providers/OpenCodeRuntimeCoordinator.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Configuration: &lt;code&gt;repos/hagicode-core/src/PCode.ClaudeHelper/AI/Configuration/OpenCodeSettings.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Proposal Archive: &lt;code&gt;openspec/changes/archive/2026-03-*opencode*/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;HagiCode's process of integrating OpenCode is actually a continuous process of stepping in pits and optimizing. From the initial standalone process mode to shared runtime, from reusing external endpoints to self-managed runtime, every architecture adjustment is driven by actual needs. Actually nothing much, just didn't miss any pit that should be stepped in.&lt;/p&gt;

&lt;p&gt;There are three core experiences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resource sharing is important&lt;/strong&gt;: Don't blindly pursue isolation, shared runtime can significantly reduce resource overhead—sometimes one person enjoying alone isn't as good as everyone using together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be careful with state management&lt;/strong&gt;: Stateful services should be self-managed, don't depend on external endpoints—after all, your own affairs are best done yourself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error recovery is essential&lt;/strong&gt;: Automatic retry mechanism can take system robustness up a level—fall down and get up, nothing big&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This solution now runs stably in HagiCode, supporting session recovery, automatic retry, runtime rebuild and other functions. If your project also needs to integrate OpenCode, hope these experiences can help you walk fewer detours. After all... only after walking detours do you know where the shortcut is, sometimes knowing it is of no use.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/anomalyco/opencode" rel="noopener noreferrer"&gt;OpenCode GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;HagiCode Official Site: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HagiCode Installation Guide: &lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;docs.hagicode.com/installation/docker-compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HagiCode Desktop: &lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;hagicode.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Official Version Demo Video: &lt;a href="https://www.bilibili.com/video/BV1z4oWB3EpY/" rel="noopener noreferrer"&gt;www.bilibili.com/video/BV1z4oWB3EpY/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opencode</category>
      <category>monorepo</category>
      <category>integration</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Steamworks Multilingual Metadata Management: From Manual Maintenance to Structured Workflow</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sat, 09 May 2026 09:00:07 +0000</pubDate>
      <link>https://dev.to/newbe36524/steamworks-multilingual-metadata-management-from-manual-maintenance-to-structured-workflow-2i9p</link>
      <guid>https://dev.to/newbe36524/steamworks-multilingual-metadata-management-from-manual-maintenance-to-structured-workflow-2i9p</guid>
      <description>&lt;h1&gt;
  
  
  Steamworks Multilingual Metadata Management: From Manual Maintenance to Structured Workflow
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;The Steam platform requires games to provide store descriptions in 28 languages. Traditional manual maintenance is inefficient and error-prone. This article introduces how to build a structured multilingual metadata management system through HagiCode, achieving an integrated workflow from content creation to export and release.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;The Steam platform requires games and applications to provide multilingual store descriptions, including fields like &lt;code&gt;about&lt;/code&gt; (detailed description) and &lt;code&gt;short_description&lt;/code&gt; (short description). For products released globally, localization content in 28 languages is typically required.&lt;/p&gt;

&lt;p&gt;This sounds like a simple content management task, but when you actually start working on it, you discover there are more problems than you imagined.&lt;/p&gt;

&lt;p&gt;First, the maintenance workload is enormous. 28 languages multiplied by 2 fields equals 56 content blocks that need to be managed. Manually switching languages for editing in the Steamworks website backend is indeed inefficient. Every content update requires repeating this process—it's painful to even talk about it.&lt;/p&gt;

&lt;p&gt;Second, scattered content is difficult to manage. Multilingual content is typically scattered across different tools and documents, lacking a unified local storage format. Version control becomes difficult, and team collaboration is prone to errors. After all, scattered things are like scattered memories—when you want to find them, you can't.&lt;/p&gt;

&lt;p&gt;Furthermore, DLC content and main application content management are siloed. If your game has multiple DLCs, each DLC needs to maintain multilingual content separately, and management complexity grows exponentially. It's like life—things pile up, and you don't know where to start cleaning up.&lt;/p&gt;

&lt;p&gt;Finally, the export format is unintuitive. The JSON format required by Steamworks doesn't match human reading habits, making manual editing error-prone. After all, who wants to look at that dense JSON?&lt;/p&gt;

&lt;p&gt;These were all problems we encountered during the actual development of the HagiCode project. As an AI coding tool for global development, we need to maintain complete multilingual content for the Steam platform. Traditional maintenance methods could no longer meet our needs, and we urgently needed a more efficient solution. Actually, there's no other way—we had to build it ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI coding tool that supports multiple AI providers and code editors. During development, we needed to maintain multilingual store content for the Steam platform, which drove us to build a structured metadata management system.&lt;/p&gt;

&lt;p&gt;The multilingual metadata management solution shared in this article is exactly what we actually refined through trial and optimization during HagiCode development. If you find this solution valuable, it shows our engineering strength is pretty good—so HagiCode itself is worth paying attention to. After all, a tool that can solve problems is a good tool, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Concepts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Languages and Fields
&lt;/h3&gt;

&lt;p&gt;Steamworks supports a fairly complete list of languages, covering major markets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;en-US, fr-FR, it-IT, de-DE, es-ES,
bg-BG, cs-CZ, da-DK, nl-NL, fi-FI,
el-GR, hu-HU, id-ID, ja-JP, ko-KR,
nb-NO, pl-PL, pt-BR, pt-PT, ro-RO,
ru-RU, zh-CN, es-419, sv-SE, th-TH,
zh-TW, tr-TR, uk-UA, vi-VN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most commonly used are &lt;code&gt;en-US&lt;/code&gt; (English), &lt;code&gt;zh-CN&lt;/code&gt; (Simplified Chinese), &lt;code&gt;zh-TW&lt;/code&gt; (Traditional Chinese), &lt;code&gt;ja-JP&lt;/code&gt; (Japanese), and &lt;code&gt;ko-KR&lt;/code&gt; (Korean). After all, these languages cover major markets—once you get these done, the others aren't so scary.&lt;/p&gt;

&lt;p&gt;The main fields that need to be maintained include two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;about&lt;/code&gt;: Detailed description, supports rich text format&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;short_description&lt;/code&gt;: Short description, with a 300-character limit&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scope Concept
&lt;/h3&gt;

&lt;p&gt;Steam app content can be divided into two scopes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base App&lt;/strong&gt;: Main application content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DLC&lt;/strong&gt;: Downloadable content, each DLC has independent content management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction is important because DLCs typically need independent store descriptions, and a game may have multiple DLCs that need unified management. It's like life—some things are primary, some are additional, but they all need to be managed properly, or things become a mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Model Design
&lt;/h2&gt;

&lt;p&gt;The system defines a clear data model to support multilingual content management:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 28 supported language codes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STEAMWORKS_SUPPORTED_LOCALES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr-FR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;it-IT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de-DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es-ES&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-BG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cs-CZ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;da-DK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nl-NL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fi-FI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;el-GR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hu-HU&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id-ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ja-JP&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ko-KR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nb-NO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pl-PL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pt-BR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pt-PT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ro-RO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ru-RU&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es-419&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sv-SE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;th-TH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-TW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tr-TR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uk-UA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vi-VN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Supported fields&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STEAMWORKS_SUPPORTED_FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;about&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Detailed description&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;short_description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// Short description&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Content scope&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SteamworksScopeKind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dlc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few considerations in this model design—well, actually, it's just about making things a bit simpler:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use standard language code formats (like &lt;code&gt;zh-CN&lt;/code&gt; instead of &lt;code&gt;chinese&lt;/code&gt;)—after all, standard things are always more reliable&lt;/li&gt;
&lt;li&gt;Explicitly list field types for future extension—who knows if more fields will be needed later&lt;/li&gt;
&lt;li&gt;Distinguish scope types to support unified management of Base App and DLC—it's always good to keep things clear&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  File Storage Structure
&lt;/h2&gt;

&lt;p&gt;Content is stored in &lt;code&gt;.hagiclaw-data/steamworks-metadata/&lt;/code&gt; in the project directory, using a hierarchical directory structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.hagiclaw-data/
└── steamworks-metadata/
    └── default-app/
        ├── workspace.json              # Workspace configuration manifest
        ├── base/                       # Base application content
        │   ├── en-US/
        │   │   ├── about.md
        │   │   └── short_description.md
        │   ├── zh-CN/
        │   │   ├── about.md
        │   │   └── short_description.md
        │   └── ...
        └── dlc/                        # DLC content
            └── turbo-engine/
                ├── en-US/
                │   ├── about.md
                │   └── short_description.md
                └── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure design has several advantages—or at least, it's much better than the previous approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Human-readable&lt;/strong&gt;: Each content is an independent Markdown file that can be edited directly—after all, human eyes prefer to see things clearly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version control friendly&lt;/strong&gt;: Text files make it easy to track change history and compare differences—so what was changed is clear at a glance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong extensibility&lt;/strong&gt;: Adding new languages or fields only requires creating new files—like building blocks, add whatever you want&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear structure&lt;/strong&gt;: The directory structure intuitively reflects how content is organized—won't make people feel confused&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;workspace.json&lt;/code&gt; stores workspace configuration, including DLC list and language configuration information. After all, some things still need a manifest—otherwise, after a while, who remembers what they put where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown to BBCode Conversion
&lt;/h2&gt;

&lt;p&gt;Steam uses BBCode format for rich text, not standard Markdown. This brings additional workload to content creation—either write BBCode directly or manually convert it later.&lt;/p&gt;

&lt;p&gt;HagiCode's solution is: let developers create content in familiar Markdown, and the system automatically converts it to Steam BBCode. After all, people are always accustomed to what they're familiar with—why force yourself to adapt to those strange curly braces?&lt;/p&gt;

&lt;h3&gt;
  
  
  Conversion Rules
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Heading conversion&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;HagiCode&lt;/span&gt;        &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;HagiCode&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;]
##&lt;/span&gt; &lt;span class="nx"&gt;Features&lt;/span&gt;        &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;Features&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/h2&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;
&lt;span class="c1"&gt;// Text styles&lt;/span&gt;
&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;     &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/b&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;italic&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;     &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;italic&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;&lt;span class="s2"&gt;`code`&lt;/span&gt;            &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/code&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;
&lt;span class="c1"&gt;// Links and images&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/url&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{STEAM_APP_IMAGE}/extras/...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;
&lt;span class="c1"&gt;// Lists&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;          &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
                   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapped&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Language Wrapping
&lt;/h3&gt;

&lt;p&gt;When exporting, content needs to be wrapped with language tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;wrapWithSteamLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SteamworksLocaleCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bbcode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Returns [lang=english]...[/lang] format&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Language codes need to be mapped to Steam's format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;en-US&lt;/code&gt; → &lt;code&gt;english&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zh-CN&lt;/code&gt; → &lt;code&gt;schinese&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zh-TW&lt;/code&gt; → &lt;code&gt;tchinese&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ja-JP&lt;/code&gt; → &lt;code&gt;japanese&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ko-KR&lt;/code&gt; → &lt;code&gt;korean&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mapping relationship isn't actually that complicated, it just needs to be remembered. After all, every platform has its own rules, we can only adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export Format
&lt;/h2&gt;

&lt;p&gt;The exported JSON needs to meet Steamworks' structure requirements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"itemid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1158573"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"languages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"english"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"app[content][about]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[h1]HagiCode[/h1]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;[b]About[/b]..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"app[content][short_description]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AI coding tool..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schinese"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"app[content][about]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[h1]HagiCode[/h1]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;[b]关于[/b]..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"app[content][short_description]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AI 编码工具..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key points aren't many, just need to remember these format requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;itemid&lt;/code&gt; corresponds to Steam AppID&lt;/li&gt;
&lt;li&gt;Steam's language codes (like &lt;code&gt;schinese&lt;/code&gt;) are used under &lt;code&gt;languages&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Field paths use &lt;code&gt;app[content][fieldName]&lt;/code&gt; format&lt;/li&gt;
&lt;li&gt;Values are converted BBCode strings&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These rules seem a bit tedious, but you get used to them. After all, every platform has its own temperament, we can only adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Service Design
&lt;/h2&gt;

&lt;p&gt;The system provides a complete REST API to support the multilingual content management workflow:&lt;/p&gt;

&lt;h3&gt;
  
  
  Load Workspace
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns workspace configuration, all languages, and field content. After all, there needs to be a place to pull everything out for viewing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Save Content
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scopeId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scopeKind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;values&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Markdown content...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;short_description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Short text...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Markdown 内容...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;short_description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;简短文本...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When saving, the system writes Markdown content to corresponding &lt;code&gt;.md&lt;/code&gt; files. This way nothing gets lost—after all, memory is always unreliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Render Preview
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;# HagiCode&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;这是关于...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns Markdown rendering result and BBCode conversion result for easy previewing. Preview is like looking in a mirror—you should at least see what you look like before going out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Export JSON
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scopeId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scopeKind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generates Steamworks-format JSON that can be directly imported into the Steamworks backend. This step is essentially packaging everything up, ready for shipping.&lt;/p&gt;

&lt;h3&gt;
  
  
  DLC Management
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dlc&lt;/span&gt;    &lt;span class="c1"&gt;// Create&lt;/span&gt;
&lt;span class="nx"&gt;PUT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dlc&lt;/span&gt;     &lt;span class="c1"&gt;// Update&lt;/span&gt;
&lt;span class="nx"&gt;DELETE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dlc&lt;/span&gt;  &lt;span class="c1"&gt;// Delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DLC management includes creating, updating, and deleting DLC metadata configurations. After all, DLC is also content and needs to be managed properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Access Metadata Panel
&lt;/h3&gt;

&lt;p&gt;Open the Steamworks Metadata panel in the HagicLaw workspace, and the system will load the current workspace's configuration and content. Once all preparations are done, you can begin.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Select Edit Scope
&lt;/h3&gt;

&lt;p&gt;Select Base App or a specific DLC in the left navigation. Each scope independently manages its multilingual content. Like organizing a room—first categorize things, then clean them up one by one.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multilingual Matrix Editing
&lt;/h3&gt;

&lt;p&gt;Expand the languages you need to edit, and directly edit the Markdown content for &lt;code&gt;about&lt;/code&gt; and &lt;code&gt;short_description&lt;/code&gt;. The system supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time Markdown rendering preview&lt;/li&gt;
&lt;li&gt;Steam BBCode conversion preview&lt;/li&gt;
&lt;li&gt;Character count and length checking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These preview features are actually quite useful—at least you can know what your content looks like. After all, no one wants to write a bunch of stuff only to find the format is completely wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Save Content
&lt;/h3&gt;

&lt;p&gt;Click the save button, and content is automatically written to corresponding &lt;code&gt;.md&lt;/code&gt; files. Files are included in Git version control for easy change tracking. Saving is like writing down memories—they won't be forgotten even after a long time.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Validation Checks
&lt;/h3&gt;

&lt;p&gt;The system automatically checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether required fields are complete&lt;/li&gt;
&lt;li&gt;Whether &lt;code&gt;short_description&lt;/code&gt; exceeds 300 characters&lt;/li&gt;
&lt;li&gt;Whether Markdown syntax is correct&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks can avoid some basic errors—after all, humans make mistakes, it's always good to have a machine help watch over things.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Export JSON
&lt;/h3&gt;

&lt;p&gt;Select the scope to export (Base App or specific DLC), and the system generates Steamworks JSON containing all languages. Copy the JSON and paste it into the Steamworks backend to complete the import. Once this step is done, the entire workflow is complete. Everything is ready, just waiting for release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Language Code Mapping
&lt;/h3&gt;

&lt;p&gt;The system's &lt;code&gt;en-US&lt;/code&gt; corresponds to Steam's &lt;code&gt;english&lt;/code&gt;, and &lt;code&gt;zh-CN&lt;/code&gt; corresponds to &lt;code&gt;schinese&lt;/code&gt;. This mapping is handled automatically during export, but needs attention when manually editing JSON. After all, some things machines can help you with, but some you still need to remember yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  BBCode Limitations
&lt;/h3&gt;

&lt;p&gt;Steam only supports a subset of BBCode, and complex Markdown may not convert perfectly. It's recommended to check conversion results in preview. Preview is like looking in a mirror—check what you look like before going out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Image Paths
&lt;/h3&gt;

&lt;p&gt;Images are converted to &lt;code&gt;[img src="{STEAM_APP_IMAGE}/extras/..."]&lt;/code&gt; placeholder format. Actual images need to be uploaded separately to the Steam backend. Images are sometimes more persuasive than text, just a bit more troublesome to upload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Field Validation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;short_description&lt;/code&gt; has a strict 300-character limit. The system validates before export, but it's recommended to control length during editing. After all, writing too many characters is useless—the platform only looks at the first 300, so you have to be concise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Control
&lt;/h3&gt;

&lt;p&gt;All Markdown files can be included in Git version control for easy change history tracking and collaborative editing. It's recommended to commit changes regularly. Version control is like a time machine that lets you return to a past moment and see what you wrote then.&lt;/p&gt;

&lt;h3&gt;
  
  
  DLC Management
&lt;/h3&gt;

&lt;p&gt;DLC's &lt;code&gt;itemId&lt;/code&gt; needs to correspond to the DLC AppID in the Steamworks backend. When creating a DLC, ensure the ID is accurate. IDs are hard to change once wrong, so it's better to be careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The core challenge of Steamworks multilingual metadata management lies in how to efficiently maintain large amounts of multilingual content. Through structured data models, human-friendly file storage, and automated conversion/export workflows, we can transform this tedious process into a manageable content creation workflow.&lt;/p&gt;

&lt;p&gt;This solution has proven effective in the practice of the HagiCode project. We transformed from a manual, error-prone state to a structured, verifiable, collaborative workflow. This not only improved efficiency but also reduced human error. After all, when the tool is well-made, things become simple.&lt;/p&gt;

&lt;p&gt;If you're developing applications for the Steam platform and need to maintain multilingual content, I hope this solution can provide some inspiration. Multilingual content management doesn't have to be a painful thing—with the right tools and workflows, it can become relatively easy. Or at least, not so despair-inducing...&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://partner.steamgames.com/doc/store/localization" rel="noopener noreferrer"&gt;Steamworks Documentation - Store Metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://partner.steamgames.com/doc/store/additional_description/steamlocalization#bbcode" rel="noopener noreferrer"&gt;Steam BBCode Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;HagiCode project: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HagiCode official site: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this article helped you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give a Star on GitHub: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Visit the official site to learn more: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Watch the official demo video: &lt;a href="https://www.bilibili.com/video/BV1z4oWB3EpY/" rel="noopener noreferrer"&gt;www.bilibili.com/video/BV1z4oWB3EpY/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;One-click installation experience: &lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;docs.hagicode.com/installation/docker-compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Desktop quick installation: &lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;hagicode.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-05-09-steamworks-multilingual-metadata-management%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-05-09-steamworks-multilingual-metadata-management%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>steamworks</category>
      <category>bbcode</category>
    </item>
    <item>
      <title>Quantifying AI Cost-Benefit Analysis</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sat, 09 May 2026 02:20:13 +0000</pubDate>
      <link>https://dev.to/newbe36524/quantifying-ai-cost-benefit-analysis-8ka</link>
      <guid>https://dev.to/newbe36524/quantifying-ai-cost-benefit-analysis-8ka</guid>
      <description>&lt;h1&gt;
  
  
  Quantifying AI Cost-Benefit Analysis
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Your boss asks: "How much does it cost to equip employees with AI assistants, and is it worth it?" You can't answer, and you feel unsure. This article discusses how to calculate this clearly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;In recent years, Claude Code, GitHub Copilot, and various AI programming assistants have flooded in like a tidal wave. As a technical person, you've probably already started using them and feel genuinely more efficient—like someone handing you a ladder when you need to climb.&lt;/p&gt;

&lt;p&gt;But when it comes to discussing ROI with your boss or clients, you often hit a wall—how do you quantify that subjective feeling of "increased efficiency"? I understand this feeling. It's like when someone asks you "what do you like about her?" and you stutter for a while, only saying "I just do." That's fine, but bosses want numbers, not your feelings.&lt;/p&gt;

&lt;p&gt;And that's not the only problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ROI&lt;/strong&gt;: Is the cost of equipping the team with AI tools worth it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Efficiency Quantification&lt;/strong&gt;: How do we translate "productivity gains" across different roles and usage levels into measurable metrics?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk Assessment&lt;/strong&gt;: If competitors大规模 adopt AI, how much will our competitiveness suffer?&lt;/p&gt;

&lt;p&gt;Traditional ROI calculations often overlook two critical factors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise Total Cost Perspective&lt;/strong&gt;: Only considering salary while ignoring city differences, social insurance, housing fund, and other additional costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Economics Model&lt;/strong&gt;: Lack of a calculation framework connecting AI usage (Tokens) to actual output&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both factors are indispensable. Here's a real example: For the same 300k annual salary, the actual cost to the enterprise in Beijing versus Wuhan can differ by over 30%. And that's not even counting the cost of AI usage itself. Cost is like an iceberg—you only ever see the tip...&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project.&lt;/p&gt;

&lt;p&gt;HagiCode is essentially just an AI code assistant project. However, during development, we genuinely needed to accurately assess the cost-effectiveness of different AI models—after all, money doesn't grow on trees. To that end, we built a complete calculation framework and open-sourced the &lt;a href="https://cost.hagicode.com" rel="noopener noreferrer"&gt;HagiCode Cost&lt;/a&gt; assessment tool.&lt;/p&gt;

&lt;p&gt;If you're also thinking about AI cost issues, this approach might give you some reference. Or maybe not—I can't guarantee that, but we're just giving it a try.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Calculation Framework
&lt;/h2&gt;

&lt;p&gt;A complete AI cost-benefit assessment requires establishing a three-layer model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input Layer
├── Annual salary data
├── City tier coefficient
├── AI model selection
├── Efficiency multiplier estimate
└── Daily Token usage

Calculation Layer
├── Enterprise total cost accounting
├── AI annual cost calculation
├── Cost proportion analysis
├── ROI calculation
└── Equivalent workforce conversion

Output Layer
├── AI cost proportion
├── Efficiency gain
├── Return on investment
├── Equivalent workforce count
└── Elimination risk assessment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This framework looks complex enough to make your head spin. Actually, the core logic is quite simple: calculate the enterprise's real labor costs clearly, calculate the AI's annual costs clearly, then look at the ROI and equivalent workforce. After all, simplifying complexity is the right path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculating Key Metrics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enterprise Annual Total Labor Cost
&lt;/h3&gt;

&lt;p&gt;First, enterprise total cost—this isn't simply annual salary multiplied by 12 months. Real costs need to consider two factors:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;City Coefficient&lt;/strong&gt;: Additional costs in first-tier cities (Beijing, Shanghai, Guangzhou, Shenzhen) are about 30% higher than other cities. This includes social insurance, housing fund, various benefits, and the cost-of-living premium for first-tier cities—after all, the price of living in Beijing versus Wuhan is indeed different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional Employment Costs&lt;/strong&gt;: Roughly equivalent to 1 month's salary, covering year-end bonuses, various subsidies, office equipment amortization, etc. These amounts may seem small individually, but they add up.&lt;/p&gt;

&lt;p&gt;So the formula is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;enterpriseAnnualTotalLaborCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;annualSalary&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;cityCoefficient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;annualSalary&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;City coefficient can refer to this standard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First-tier cities (Beijing, Shanghai, Guangzhou, Shenzhen): 0.4&lt;/li&gt;
&lt;li&gt;New first-tier (Hangzhou, Chengdu, Suzhou, Nanjing): 0.3&lt;/li&gt;
&lt;li&gt;Second-tier cities (Wuhan, Xi'an, Tianjin, Zhengzhou): 0.2&lt;/li&gt;
&lt;li&gt;Other cities: 0.1&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI Annual Cost
&lt;/h3&gt;

&lt;p&gt;AI cost calculation is slightly more complex because AI models charge by Token. And input and output prices differ—output is typically 5-10x more expensive than input. This isn't surprising, after all, output is the AI "working," while input is just you "talking."&lt;/p&gt;

&lt;p&gt;In code scenarios, the input-output ratio is about 3:1, so we can calculate a composite unit price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Composite unit price (based on 3:1 input-output ratio)&lt;/span&gt;
&lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nx"&gt;inputPrice&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;outputPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

&lt;span class="c1"&gt;// Daily cost&lt;/span&gt;
&lt;span class="nx"&gt;dailyAICost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dailyTokenUsage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt;

&lt;span class="c1"&gt;// Annual cost (based on 264 working days)&lt;/span&gt;
&lt;span class="nx"&gt;annualAICost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dailyAICost&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;264&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, GPT-5.4's input price is 2.5 USD/1M Token, output price is 15 USD/1M Token. Then the composite unit price is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.625&lt;/span&gt; &lt;span class="nx"&gt;USD&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt; &lt;span class="nx"&gt;Token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Converting to RMB (assuming 1 USD = 7.25 CNY):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.625&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;7.25&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;40.78&lt;/span&gt; &lt;span class="nx"&gt;yuan&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt; &lt;span class="nx"&gt;Token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exchange rates fluctuate, but we fix them for calculation convenience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Benefit Metrics
&lt;/h3&gt;

&lt;p&gt;With the two costs above, we can calculate core metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI cost proportion&lt;/span&gt;
&lt;span class="nx"&gt;aiCostProportion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;annualAICost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;enterpriseAnnualTotalLaborCost&lt;/span&gt;

&lt;span class="c1"&gt;// Efficiency gain&lt;/span&gt;
&lt;span class="nx"&gt;efficiencyGain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;efficiencyMultiplier&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;// AI return on investment&lt;/span&gt;
&lt;span class="nx"&gt;aiROI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;efficiencyGain&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;aiCostProportion&lt;/span&gt;

&lt;span class="c1"&gt;// Affordable workflow count&lt;/span&gt;
&lt;span class="nx"&gt;affordableCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;enterpriseAnnualTotalLaborCost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;annualAICost&lt;/span&gt;

&lt;span class="c1"&gt;// Equivalent workforce&lt;/span&gt;
&lt;span class="nx"&gt;equivalentWorkforce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;efficiencyMultiplier&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;affordableCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanings of these metrics:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Cost Proportion&lt;/strong&gt;: The percentage of enterprise labor costs consumed to maintain Agent workflows. The lower this number, the more "cost-effective" the AI usage. Who doesn't like saving money?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Return on Investment&lt;/strong&gt;: Efficiency gain ÷ AI cost proportion. Less than 1 means "somewhat wasteful," greater than 2 means "very worthwhile." This is easy to understand—like spending money to buy time, you calculate whether it's worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Equivalent Workforce&lt;/strong&gt;: There's a point easily misunderstood here. It's not directly accepting the efficiency multiplier, but whether the enterprise can afford this AI workflow. If affordableCount is less than 1, then equivalent workforce won't reach your expected efficiency multiplier. After all, even the cleverest housewife can't cook without rice...&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Calculation Example
&lt;/h2&gt;

&lt;p&gt;Let's do a real accounting example. Assume a first-tier city backend developer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Annual salary: 300k&lt;/li&gt;
&lt;li&gt;Using GPT-5.4, efficiency multiplier: 2.5x&lt;/li&gt;
&lt;li&gt;Daily Token usage: 12 M&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Calculate enterprise total cost&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;enterpriseTotalCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;44.5&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Calculate AI annual cost&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;40.78&lt;/span&gt; &lt;span class="nx"&gt;yuan&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt; &lt;span class="nx"&gt;Token&lt;/span&gt;
&lt;span class="nx"&gt;dailyCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;40.78&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;489.36&lt;/span&gt; &lt;span class="nx"&gt;yuan&lt;/span&gt;
&lt;span class="nx"&gt;annualCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;489.36&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;264&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;129&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;191&lt;/span&gt; &lt;span class="nx"&gt;yuan&lt;/span&gt; &lt;span class="err"&gt;≈&lt;/span&gt; &lt;span class="mf"&gt;12.9&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Calculate benefit metrics&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;AI&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="nx"&gt;proportion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;12.9&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;44.5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="nx"&gt;efficiencyGain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;investment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.29&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.17&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Calculate equivalent workforce&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;affordableCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;44.5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;12.9&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.45&lt;/span&gt;
&lt;span class="nx"&gt;equivalentWorkforce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="nx"&gt;people&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's the conclusion? This AI usage has an ROI over 5, falling in the "very worthwhile" range. If the entire team uses it, forming approximately 2.5 people's production capacity advantage, it would be very competitive in the market.&lt;/p&gt;

&lt;p&gt;This makes sense—after all, the money you spend on AI is far less than your additional output. This deal is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact of Multi-Agent
&lt;/h2&gt;

&lt;p&gt;HagiCode discovered an interesting phenomenon in actual use: a single Agent's efficiency gains have an upper limit.&lt;/p&gt;

&lt;p&gt;This is actually quite natural—no matter how capable a person is, they can only do one thing at a time. After all, you're not an octopus.&lt;/p&gt;

&lt;p&gt;Traditional single Agent usage patterns have several bottlenecks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serial Limitation&lt;/strong&gt;: Proposal → Implementation → Review → Fix must wait sequentially. No matter how fast a single Agent is, it can only do one thing at a time. It's like cooking—you can only wash, cut, and stir-fry step by step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quota Waste&lt;/strong&gt;: Monthly quota limits can't be fully utilized. Unused quota this month doesn't roll over to next month. This isn't surprising, just a bit wasteful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context Switching&lt;/strong&gt;: Different tasks require repeatedly establishing context, meaning you have to explain background information each time. Like chatting with different people about the same thing—starting from scratch each time gets tiring.&lt;/p&gt;

&lt;p&gt;HagiCode's multi-Agent architecture solves these problems through parallel sessions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallel 10x+&lt;/strong&gt;: Multiple Agents drive multiple instances simultaneously, achieving true parallel work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput Increase&lt;/strong&gt;: Proposal, implementation, and fixes can advance in parallel without waiting for each other&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved Token Utilization&lt;/strong&gt;: OpenSpec process reduces rework, spreading equivalent consumption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The change this brings is enormous. Using the previous example, if using HagiCode multi-Agent architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parallel sessions: 4&lt;/li&gt;
&lt;li&gt;Token utilization improvement: 1.5x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Amplified calculation&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;amplifiedEfficiency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;
&lt;span class="nx"&gt;optimizedDailyToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="nx"&gt;M&lt;/span&gt;
&lt;span class="nx"&gt;optimizedAnnualCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;40.78&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;264&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;344&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;New benefit metrics&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;newAICostProportion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;34.4&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;44.5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="nx"&gt;newROI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.77&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;11.68&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;
&lt;span class="nx"&gt;newEquivalentWorkforce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="nx"&gt;people&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although AI cost proportion rose from 29% to 77%, ROI increased from 5.17x to 11.68x, and equivalent workforce changed from 2.5 to 10 people.&lt;/p&gt;

&lt;p&gt;This is the power of multi-Agent parallelism. One Agent is one person; ten Agents are a team... The difference isn't just a little bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Don't Get City Coefficient Wrong
&lt;/h3&gt;

&lt;p&gt;Employment cost differences across cities are significant—first-tier cities' additional costs are about 30% higher than other cities. When calculating, be sure to use the correct city tier. A small difference in this number can significantly skew the final result. After all, "a miss is as good as a mile"... This is an old saying, but it still holds true.&lt;/p&gt;

&lt;h3&gt;
  
  
  Input-Output Ratio Isn't Fixed
&lt;/h3&gt;

&lt;p&gt;Code scenarios default to a 3:1 input-output ratio, matching the proportion of prompts to generated code in actual programming. But if you're doing other types of work—like writing copy or doing data analysis—this ratio might be completely different.&lt;/p&gt;

&lt;p&gt;This is normal—different work, different methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Efficiency Multiplier Is Subjective
&lt;/h3&gt;

&lt;p&gt;Efficiency multiplier is a subjective estimate. It's recommended to combine with actual observation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.5-2x: Familiar with basic functions, occasional use&lt;/li&gt;
&lt;li&gt;2-3x: Proficient, daily high-frequency use&lt;/li&gt;
&lt;li&gt;3x+: Deep integration, forming专属 workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't estimate too high initially—observe for a while before adjusting. After all, higher expectations lead to greater disappointment.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Calculate Token Usage
&lt;/h3&gt;

&lt;p&gt;If you don't know your daily Token usage, you can estimate this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check platform usage statistics (both Claude and OpenAI have them)&lt;/li&gt;
&lt;li&gt;Record Token consumption from several typical conversations and take an average&lt;/li&gt;
&lt;li&gt;Multiply by your daily conversation count&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or just use &lt;a href="https://cost.hagicode.com" rel="noopener noreferrer"&gt;HagiCode Cost&lt;/a&gt; to calculate—it has reference values for common scenarios. This is convenient and saves you from blind trial and error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Impact of Exchange Rate Fluctuations
&lt;/h3&gt;

&lt;p&gt;USD models require exchange rate conversion, but rates fluctuate. Calculators typically use fixed rates (like 1 USD = 7.25 CNY), while actual costs may vary with exchange rate fluctuations. This error is usually small, but keep it in mind.&lt;/p&gt;

&lt;p&gt;After all, everything has an approximation—precision to several decimal places isn't really necessary...&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Implementation Points
&lt;/h2&gt;

&lt;p&gt;If you want to implement this calculation logic yourself, several technical details are worth noting:&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Currency Support
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertCnyAmountToCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;amountCny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;targetCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CNY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetCurrency&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CNY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;amountCny&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;amountCny&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;EXCHANGE_RATE_USD_TO_CNY&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's not much to say about this code—it's just simple currency conversion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Language Localization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getLocalizedModelCopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModelPricing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SupportedLanguage&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;LocalizedModelMeta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;descriptionEn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pricingContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pricingContext&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pricingContextEn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other fields&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multi-language support is complex in some ways, simple in others. It's essentially storing different language content and retrieving it when needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regional Differentiation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCityTierLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;cityTier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CityTier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cn-mainland&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;international&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SupportedLanguage&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;benchmarkData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cityCoefficients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;cityTier&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cn-mainland&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;labelEn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internationalLabel&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internationalLabelEn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regional differentiation means displaying different labels for different regions. This isn't difficult—just judge the region and language, then return the corresponding value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;AI cost-benefit assessment isn't anything profound—the core is three calculations: enterprise labor costs, AI usage costs, and efficiency improvement magnitude. Calculate these three clearly, and the ROI naturally emerges.&lt;/p&gt;

&lt;p&gt;This is like many things in life—seemingly complex, but when broken down, it's just that. Few people are willing to sit down and calculate it.&lt;/p&gt;

&lt;p&gt;But there's an easily overlooked point here: the multiplier effect from multi-Agent architecture. No matter how strong a single Agent is, it can only improve efficiency linearly. But multiple Agents working in parallel bring exponential capacity improvements. This is the core reason HagiCode chose a multi-Agent architecture.&lt;/p&gt;

&lt;p&gt;One person's power is limited; a group's power is infinite. This sounds like a platitude, but applied to AI, it's fitting.&lt;/p&gt;

&lt;p&gt;If you're also thinking about AI cost issues, welcome to try &lt;a href="https://cost.hagicode.com" rel="noopener noreferrer"&gt;HagiCode Cost&lt;/a&gt; to experience our calculator. Or go directly to &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; to see the source code—maybe it'll give you some inspiration.&lt;/p&gt;

&lt;p&gt;Or maybe not—I can't guarantee that. Just giving it a try, after all, paths are made by walking...&lt;/p&gt;




&lt;p&gt;Writing this, I suddenly remembered an old saying: &lt;strong&gt;"To do good work, one must first sharpen one's tools."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But sometimes, even with sharp tools, knowing how to use them is another matter. AI is like a double-edged sword—used well, it's assistance; used poorly, it's a burden. The balance is for you to find.&lt;/p&gt;

&lt;p&gt;Enough of that. Hope this helps you.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cost.hagicode.com" rel="noopener noreferrer"&gt;HagiCode Cost - AI Cost Assessment Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.com/api/pricing/" rel="noopener noreferrer"&gt;OpenAI Official Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic Claude Pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>hagicode</category>
      <category>roi</category>
    </item>
  </channel>
</rss>
