11using System . Text ;
2+ using System . Collections . Concurrent ;
23using System . Text . Json ;
34using System . Text . Json . Nodes ;
45using System . Text . Json . Serialization ;
@@ -32,13 +33,10 @@ async Task WriteProtocolAsync(object message)
3233using var host = new AgentOsBridgeHost (
3334 ( eventName , payload ) => WriteProtocolAsync ( new BridgeNotification ( eventName , payload ) ) ) ;
3435
35- while ( await Console . In . ReadLineAsync ( ) is { } line )
36- {
37- if ( string . IsNullOrWhiteSpace ( line ) )
38- {
39- continue ;
40- }
36+ var inFlightRequests = new List < Task > ( ) ;
4137
38+ async Task ProcessRequestAsync ( string line )
39+ {
4240 BridgeResponse response ;
4341 try
4442 {
@@ -69,6 +67,24 @@ async Task WriteProtocolAsync(object message)
6967 await WriteProtocolAsync ( response ) ;
7068}
7169
70+ while ( await Console . In . ReadLineAsync ( ) is { } line )
71+ {
72+ if ( string . IsNullOrWhiteSpace ( line ) )
73+ {
74+ continue ;
75+ }
76+
77+ // A model run can legitimately stay active for many minutes. Processing
78+ // bridge messages serially made every health, recovery and start request
79+ // wait behind that run, so the Electron shell reported a false start_task
80+ // timeout. Keep protocol writes ordered through outputGate, but allow
81+ // independent AgentOS requests to progress concurrently.
82+ inFlightRequests . RemoveAll ( request => request . IsCompleted ) ;
83+ inFlightRequests . Add ( ProcessRequestAsync ( line ) ) ;
84+ }
85+
86+ await Task . WhenAll ( inFlightRequests ) ;
87+
7288internal sealed record BridgeRequest ( string Id , string Method , JsonObject ? Params ) ;
7389internal sealed record BridgeResponse ( string Id , object ? Result , BridgeError ? Error ) ;
7490internal sealed record BridgeError ( string Code , string Message ) ;
@@ -91,8 +107,13 @@ internal sealed class AgentOsBridgeHost : IDisposable
91107 private readonly LivingMemoryService _livingMemory ;
92108 private readonly EvolutionLabService _evolutionLab ;
93109 private readonly RemoteCapabilityStoreService _remoteStore ;
94- private readonly Dictionary < string , TaskItem > _active =
110+ private readonly ConcurrentDictionary < string , TaskItem > _active =
111+ new ( StringComparer . OrdinalIgnoreCase ) ;
112+ private readonly ConcurrentDictionary < string , SemaphoreSlim > _startGates =
95113 new ( StringComparer . OrdinalIgnoreCase ) ;
114+ private readonly ConcurrentDictionary < string , byte > _agentRuns =
115+ new ( StringComparer . OrdinalIgnoreCase ) ;
116+ private readonly SemaphoreSlim _bootGate = new ( 1 , 1 ) ;
96117 private bool _booted ;
97118
98119 public AgentOsBridgeHost ( Func < string , object , Task > publish )
@@ -147,18 +168,32 @@ public AgentOsBridgeHost(Func<string, object, Task> publish)
147168
148169 private async Task < object > BootAsync ( )
149170 {
150- if ( ! _booted )
171+ if ( _booted )
172+ {
173+ return ProjectKernel ( ) ;
174+ }
175+
176+ await _bootGate . WaitAsync ( ) ;
177+ try
178+ {
179+ if ( ! _booted )
180+ {
181+ var boot = await _kernel . BootAsync ( ) ;
182+ await _supervisor . BootAsync ( boot . BootId ) ;
183+ await _kernel . ReportServiceAsync (
184+ "supervisor" ,
185+ "Agent Supervisor" ,
186+ AgentOsServiceHealth . Ready ,
187+ "Electron bridge lease layer active" ,
188+ boot . BootId ) ;
189+ _booted = true ;
190+ }
191+ }
192+ finally
151193 {
152- var boot = await _kernel . BootAsync ( ) ;
153- await _supervisor . BootAsync ( boot . BootId ) ;
154- await _kernel . ReportServiceAsync (
155- "supervisor" ,
156- "Agent Supervisor" ,
157- AgentOsServiceHealth . Ready ,
158- "Electron bridge lease layer active" ,
159- boot . BootId ) ;
160- _booted = true ;
194+ _bootGate . Release ( ) ;
161195 }
196+
162197 return ProjectKernel ( ) ;
163198 }
164199
@@ -288,6 +323,42 @@ private async Task<object> StartTaskAsync(JsonObject parameters)
288323 ? parsedMode
289324 : AgentExecutionMode . Ask ;
290325 var requestedTaskId = OptionalString ( parameters , "taskId" ) ;
326+ var startKey = requestedTaskId ?? "new-" + Guid . NewGuid ( ) . ToString ( "N" ) ;
327+ var startGate = _startGates . GetOrAdd ( startKey , _ => new SemaphoreSlim ( 1 , 1 ) ) ;
328+ await startGate . WaitAsync ( ) ;
329+ try
330+ {
331+ // A timed-out Electron call may have completed inside AgentOS and
332+ // retained the lease. Before run_agent begins, returning that same
333+ // active task is the safe idempotent response; acquiring a second
334+ // lease would incorrectly report a conflict with our own host.
335+ if ( requestedTaskId is not null
336+ && _active . TryGetValue ( requestedTaskId , out var activeTask ) )
337+ {
338+ if ( _agentRuns . ContainsKey ( requestedTaskId ) )
339+ {
340+ throw new InvalidOperationException (
341+ $ "Task { requestedTaskId } is already executing. "
342+ + "Wait for the active run or cancel it before retrying." ) ;
343+ }
344+
345+ return ProjectTask ( activeTask ) ;
346+ }
347+
348+ return await StartTaskCoreAsync ( parameters , prompt , mode , requestedTaskId ) ;
349+ }
350+ finally
351+ {
352+ startGate . Release ( ) ;
353+ }
354+ }
355+
356+ private async Task < object > StartTaskCoreAsync (
357+ JsonObject parameters ,
358+ string prompt ,
359+ AgentExecutionMode mode ,
360+ string ? requestedTaskId )
361+ {
291362 var recovered = requestedTaskId is null
292363 ? null
293364 : _snapshots . LoadAll ( ) . FirstOrDefault ( item =>
@@ -419,12 +490,18 @@ await _supervisor.HeartbeatAsync(
419490 private async Task < object > RunAgentAsync ( JsonObject parameters )
420491 {
421492 var task = GetActiveTask ( RequiredString ( parameters , "taskId" ) ) ;
493+ if ( ! _agentRuns . TryAdd ( task . Id , 0 ) )
494+ {
495+ throw new InvalidOperationException (
496+ $ "Task { task . Id } already has an active Agent run.") ;
497+ }
422498 var prompt = RequiredString ( parameters , "prompt" ) ;
423499 var apiKey = OptionalString ( parameters , "apiKey" ) ?? string . Empty ;
424500 var endpoint = OptionalString ( parameters , "endpoint" ) ;
425501 var approvalMode = OptionalString ( parameters , "approvalMode" ) ?? "readOnly" ;
426502 var attachments = ParseAttachments ( parameters [ "attachments" ] as JsonArray ) ;
427503 var conversationContext = BuildConversationContext (
504+ task . Id ,
428505 parameters [ "conversation" ] as JsonArray ,
429506 prompt ) ;
430507 task . Attachments = attachments ;
@@ -591,68 +668,31 @@ await PublishEventCoreAsync(new AgentRuntimeEvent(
591668 } ;
592669 }
593670
594- private static string BuildConversationContext (
671+ private string BuildConversationContext (
672+ string taskId ,
595673 JsonArray ? values ,
596674 string currentPrompt )
597675 {
598- if ( values is null || values . Count < 2 )
599- {
600- return string . Empty ;
601- }
602-
603- var turns = values
676+ var turns = values ?
604677 . OfType < JsonObject > ( )
605- . Select ( value => new
606- {
607- Role = OptionalString ( value , "role" ) ? . Equals (
678+ . Select ( ( value , index ) => new ConversationTurn (
679+ $ "transient-{ index } ",
680+ taskId ,
681+ OptionalString ( value , "role" ) ? . Equals (
608682 "assistant" ,
609683 StringComparison . OrdinalIgnoreCase ) == true
610- ? "ASSISTANT "
611- : "USER " ,
612- Content = OptionalString ( value , "content" ) ?? string . Empty
613- } )
684+ ? "assistant "
685+ : "user " ,
686+ OptionalString ( value , "content" ) ?? string . Empty ,
687+ DateTimeOffset . UnixEpoch . AddSeconds ( index ) ) )
614688 . Where ( turn => ! string . IsNullOrWhiteSpace ( turn . Content ) )
615- . ToList ( ) ;
616- if ( turns . Count > 0
617- && turns [ ^ 1 ] . Role == "USER"
618- && turns [ ^ 1 ] . Content . Trim ( ) . Equals (
619- currentPrompt . Trim ( ) ,
620- StringComparison . Ordinal ) )
621- {
622- turns . RemoveAt ( turns . Count - 1 ) ;
623- }
624- if ( turns . Count == 0 )
625- {
626- return string . Empty ;
627- }
628-
629- const int maximumCharacters = 48_000 ;
630- var selected = new List < ( string Role , string Content ) > ( ) ;
631- var used = 0 ;
632- for ( var index = turns . Count - 1 ; index >= 0 ; index -- )
633- {
634- var turn = turns [ index ] ;
635- var bounded = LimitForReview ( turn . Content , 12_000 ) ;
636- if ( used + bounded . Length > maximumCharacters && selected . Count > 0 )
637- {
638- break ;
639- }
640- selected . Add ( ( turn . Role , bounded ) ) ;
641- used += bounded . Length ;
642- }
643- selected . Reverse ( ) ;
644- var builder = new StringBuilder (
645- "[NOVA CONTINUOUS CONVERSATION CONTEXT]\n "
646- + "以下是同一任务较早轮次的真实对话。延续已确认目标与术语;"
647- + "若历史与当前工作区冲突,以当前工具读取结果为准。\n " ) ;
648- foreach ( var turn in selected )
649- {
650- builder . AppendLine ( $ "<{ turn . Role } >") ;
651- builder . AppendLine ( turn . Content ) ;
652- builder . AppendLine ( $ "</{ turn . Role } >") ;
653- }
654- builder . AppendLine ( "[CURRENT USER REQUEST FOLLOWS]" ) ;
655- return builder . ToString ( ) ;
689+ . ToArray ( )
690+ ?? [ ] ;
691+ return _conversations . BuildContextPrompt (
692+ taskId ,
693+ currentPrompt ,
694+ turns ,
695+ includeCurrentPrompt : false ) ;
656696 }
657697
658698 private async Task < object > VerifyResultAsync ( JsonObject parameters )
@@ -879,7 +919,8 @@ await _journal.AppendAsync(
879919 }
880920 finally
881921 {
882- _active . Remove ( task . Id ) ;
922+ _agentRuns . TryRemove ( task . Id , out _ ) ;
923+ _active . TryRemove ( task . Id , out _ ) ;
883924 _governor . EndTask ( task . Id ) ;
884925 }
885926 }
0 commit comments