| Meta | Giá trị |
|---|---|
| Trace | 2026-07-18 · flow-trace (grpc-go) — golden-flow run đầu tiên của skill sinh bởi flow-trace-genesis v0.1.0 |
| Repo & commit | grpc-go@7d870454 (master) — module google.golang.org/grpc, go 1.25.0 |
| Anchor | call.go:29 — ClientConn.Invoke |
| Trạng thái | Draft — chờ user chấm golden-flow gate |
Generated code (stub .pb.go) gọi cc.Invoke cho mọi unary RPC. Invoke gom call options rồi đi qua interceptor chain (nếu có) trước khi vào invoke — hàm này chỉ làm đúng 3 việc: newClientStream → SendMsg(req) → RecvMsg(reply).
newClientStream là nơi nặng nhất: đếm call cho channelz/idleness, validate metadata, chờ resolver trả địa chỉ đầu tiên, hỏi config selector lấy method config (timeout, retry policy, max size), chọn codec/compressor, rồi tạo attempt đầu tiên: qua pickerWrapper.pick (block tới khi balancer có picker sẵn sàng) lấy transport, mở stream HTTP/2 (http2Client.NewStream — header grpc-timeout sinh từ deadline).
SendMsg marshal + nén + kiểm tra size, buffer payload cho retry, rồi ghi frame qua transport. RecvMsg dựng decompressor theo header trả về, decode message, và đọc thêm một lần nữa để bắt EOF (cardinality check). Retry engine bọc mọi op: transparent retry khi stream chưa được server xử lý, policy retry theo service config (throttling + server pushback + backoff jitter).
Với unary RPC, lỗi ghi xuống transport bị nuốt tại SendMsg (trả nil) — lỗi thật chỉ lộ ra ở RecvMsg qua status của stream (stream.go:1147-1153, comment ghi rõ "because the generated code requires it"). Ai đọc code caller mà tưởng SendMsg == nil nghĩa là đã gửi thành công sẽ hiểu sai hoàn toàn semantics.
Graph này do render/kg/extract.py sinh từ index GitNexus của grpc-go (k-hop ≤2 quanh seeds), KHÔNG qua Read từng edge. Node/cạnh sáng + nét liền = trùng seeds lấy từ bảng bước (đã Read, có file:line ở section 04); phần mờ + nét đứt = ứng viên máy gợi ý — muốn dùng làm căn cứ phải Read xác nhận trước.
Ghi chú phạm vi: single-repo (grpc-go); seeds là xương sống unary Invoke path (Invoke, newClientStream, clientStream, csAttempt, pickerWrapper, http2Client). Edge sang server-side/wire không có trong graph — xem sequence diagram (section 03) cho ranh giới HTTP/2.
Mermaid — nguồn đầy đủ
sequenceDiagram
autonumber
participant GC as generated code
participant IV as ClientConn.Invoke (call.go)
participant CS as clientStream (stream.go)
participant PW as pickerWrapper
participant AT as csAttempt
participant T as http2Client
participant W as HTTP/2 wire
GC->>IV: Invoke(ctx, method, req, reply, opts)
Note over IV: combine(callOptions) :32<br/>unaryInt chain? :34-36
IV->>CS: newClientStream (unaryStreamDesc)
Note over CS: OnCallBegin :227 · metadata validate :233-246<br/>waitForResolvedAddrs :249<br/>configSelector → methodConfig :263-297<br/>timeout ctx :313-318 · codec/compressor :330-364
CS->>AT: op = getTransport + newStream (withRetry :399-412)
AT->>PW: pick(ctx, failfast, method) :519
Note over PW: block trên blockingCh tới khi<br/>picker sẵn sàng :111-140
PW-->>AT: pick{transport, result}
AT->>T: NewStream(callHdr) :756
Note over T: createHeaderFields :542<br/>grpc-timeout từ deadline :613
IV->>CS: SendMsg(req)
Note over CS: prepareMsg (marshal+nén) :973<br/>size guard :990 · bufferForRetry :1007
CS->>AT: sendMsg :1138
AT->>T: transportStream.Write (Last=true) :1147
alt write lỗi (unary)
AT-->>CS: return nil :1148-1153 (lỗi hoãn sang RecvMsg)
end
T-)W: loopyWriter ghi frame (goroutine :476-479)
W-)T: reader nhận frame (goroutine :420)
IV->>CS: RecvMsg(reply)
CS->>AT: recvMsg (withRetry + commitAttempt :1035-1037)
Note over AT: decompressor từ RecvCompress :1169-1187<br/>recv :1189 · recv lần 2 bắt EOF :1227-1232
AT-->>IV: reply hoặc status error (từ trailer :1191)
alt lỗi retryable
CS->>CS: shouldRetry :701-789 (transparent :717 / policy+throttle :757-767<br/>pushback :734-747 · backoff jitter :776-782)
CS->>AT: newAttemptLocked → replay buffered ops :807-850
endMermaid source
sequenceDiagram
autonumber
participant GC as generated code
participant IV as ClientConn.Invoke (call.go)
participant CS as clientStream (stream.go)
participant PW as pickerWrapper
participant AT as csAttempt
participant T as http2Client
participant W as HTTP/2 wire
GC->>IV: Invoke(ctx, method, req, reply, opts)
Note over IV: combine(callOptions) :32<br/>unaryInt chain? :34-36
IV->>CS: newClientStream (unaryStreamDesc)
Note over CS: OnCallBegin :227 · metadata validate :233-246<br/>waitForResolvedAddrs :249<br/>configSelector → methodConfig :263-297<br/>timeout ctx :313-318 · codec/compressor :330-364
CS->>AT: op = getTransport + newStream (withRetry :399-412)
AT->>PW: pick(ctx, failfast, method) :519
Note over PW: block trên blockingCh tới khi<br/>picker sẵn sàng :111-140
PW-->>AT: pick{transport, result}
AT->>T: NewStream(callHdr) :756
Note over T: createHeaderFields :542<br/>grpc-timeout từ deadline :613
IV->>CS: SendMsg(req)
Note over CS: prepareMsg (marshal+nén) :973<br/>size guard :990 · bufferForRetry :1007
CS->>AT: sendMsg :1138
AT->>T: transportStream.Write (Last=true) :1147
alt write lỗi (unary)
AT-->>CS: return nil :1148-1153 (lỗi hoãn sang RecvMsg)
end
T-)W: loopyWriter ghi frame (goroutine :476-479)
W-)T: reader nhận frame (goroutine :420)
IV->>CS: RecvMsg(reply)
CS->>AT: recvMsg (withRetry + commitAttempt :1035-1037)
Note over AT: decompressor từ RecvCompress :1169-1187<br/>recv :1189 · recv lần 2 bắt EOF :1227-1232
AT-->>IV: reply hoặc status error (từ trailer :1191)
alt lỗi retryable
CS->>CS: shouldRetry :701-789 (transparent :717 / policy+throttle :757-767<br/>pushback :734-747 · backoff jitter :776-782)
CS->>AT: newAttemptLocked → replay buffered ops :807-850
endfile:line (grpc-go@7d870454)Chặng 1 — Entry & stream setup
| # | Package | Bước | File:line | Loại edge | Ghi chú |
|---|---|---|---|---|---|
| S1 | grpc | Generated code gọi cc.Invoke; gom call options combine(cc.dopts.callOptions, opts) | call.go:29-32 | call | combine cố ý không dùng append để tránh share capacity giữa các call song song call.go:40-53 |
| S2 | grpc | Interceptor chain: nếu dopts.unaryInt set thì bọc invoke | call.go:34-36 | call | chain dựng lúc NewClient: chainUnaryClientInterceptors clientconn.go:224,519-525 — unaryInt luôn đứng đầu |
| S3 | grpc | invoke = newClientStream → SendMsg → RecvMsg | call.go:65-74 | call | unaryStreamDesc (không client/server stream) call.go:63 |
| S4 | grpc | newClientStream: channelz incrCallsStarted, idleness OnCallBegin, callopt OnFinish giảm đếm | stream.go:214-231 | call | cleanup khi tạo stream fail: endOfClientStream defer stream.go:217-222 |
| S5 | grpc | Validate metadata outgoing (key/pair) → codes.Internal nếu sai | stream.go:233-246 | call | |
| S6 | grpc | Chờ resolver: waitForResolvedAddrs — RPC đầu tiên block tới khi có địa chỉ | stream.go:249 → clientconn.go:759 | call (block) | trả cờ nameResolutionDelayed |
| S7 | grpc | Config selector → MethodConfig (timeout, retry policy, max size) + interceptor per-RPC nếu có | stream.go:263-297 | indirection | status code từ control plane bị giới hạn theo gRFC A54 stream.go:266-273 |
| S8 | grpc | newClientStreamWithParams: ctx timeout từ mc.Timeout; callopt before hooks; max send/recv size; codec | stream.go:313-334 | call | context leak được document stream.go:308-312 (issue #1818) |
| S9 | grpc | Dựng callHdr (authority, method, content-subtype, accepted compressors) + chọn compressor (per-call UseCompressor ưu tiên hơn DialOption) | stream.go:336-364 | call | compressor không đăng ký → codes.Internal stream.go:356-358 |
| S10 | grpc | Nạp retryThrottler + binary loggers | stream.go:385-395 | call |
Chặng 2 — Attempt: pick transport & mở stream HTTP/2
| # | Package | Bước | File:line | Loại edge | Ghi chú |
|---|---|---|---|---|---|
| S11 | grpc | Op đầu tiên getTransport + newStream chạy qua withRetry, gán cs.attempt | stream.go:399-412 | call | op được buffer để replay khi retry stream.go:412 |
| S12 | grpc | getTransport → pickerWrapper.pick(ctx, failfast, PickInfo) | stream.go:515-535 | indirection | drop error từ picker đánh dấu a.drop stream.go:522-525 |
| S13 | grpc | pick: loop trên pickerGen, block trên blockingCh tới khi có picker; ctx done → map sang DeadlineExceeded/Canceled | picker_wrapper.go:105-174 | goroutine-sync | ErrNoSubConnAvailable → loop tiếp :159-161; failfast=false → block tiếp :172-174 |
| S14 | grpc | csAttempt.newStream: merge metadata từ PickResult → t.NewStream | stream.go:537-554 | call | PreviousAttempts set từ numRetries :539 |
| S15 | transport | http2Client.NewStream(callHdr): dựng header fields (createHeaderFields), đăng ký stream | internal/transport/http2_client.go:756, :542 | transport | deadline → header grpc-timeout :613 |
Chặng 3 — Send / Recv / Retry
| # | Package | Bước | File:line | Loại edge | Ghi chú |
|---|---|---|---|---|---|
| S16 | grpc | SendMsg: prepareMsg (marshal + nén qua codec/compressor), guard maxSendMessageSize → ResourceExhausted | stream.go:954-992 | call | unary set sentLast ngay :968-970 |
| S17 | grpc | Payload Ref() + withRetry(op) + bufferForRetryLocked giữ payload cho retry | stream.go:996-1012 | call | không buffer thì free ngay :1010-1012 |
| S18 | grpc | csAttempt.sendMsg → transportStream.Write(hdr, payload, Last: true) | stream.go:1138-1155 | transport | ⚠️ unary: write lỗi → return nil :1148-1153; client-stream: io.EOF :1154 |
| S19 | transport | Frame thực sự xuống socket qua goroutine nền: loopyWriter.run (ghi) + t.reader (đọc) | internal/transport/http2_client.go:476-479, :420 | goroutine | loopy lỗi non-IO → close conn :478-481 |
| S20 | grpc | RecvMsg: withRetry(recvMsg, commitAttemptLocked); kết thúc gọi finish | stream.go:1025-1051 | call | commit = từ đây không retry op cũ nữa :1037 |
| S21 | grpc | recvMsg: dựng decompressor từ header RecvCompress (một lần/stream), check compressor được phép | stream.go:1169-1187 | call | peer nén bằng thuật toán ngoài danh sách → codes.Internal :1179-1181 |
| S22 | grpc | recv decode message; io.EOF → lấy status từ trailer; không nhận message nào → cardinality violation | stream.go:1189-1202 | call | :1195-1196 |
| S23 | grpc | Unary đọc lần 2 — phải là EOF; nhận thêm message → cardinality violation | stream.go:1225-1232 | call | status cuối lấy từ transportStream.Status() :1228 |
| S24 | grpc | Retry engine shouldRetry: transparent (stream unprocessed) / policy theo RetryableStatusCodes + throttle + server pushback grpc-retry-pushback-ms + backoff jitter 0.8-1.2 | stream.go:701-789 | call | max attempts :767-768; disableRetry :721-723 |
Mermaid — nguồn đầy đủ
flowchart LR
subgraph public_api["call.go — public API"]
IV["ClientConn.Invoke :29<br/>unaryInt chain :34"] --> INV["invoke :65"]
end
subgraph engine["stream.go — stream engine"]
NCS["newClientStream :213"] --> NCSP["newClientStreamWithParams :302"]
NCSP --> CSTR["clientStream<br/>withRetry :831 · SendMsg :954 · RecvMsg :1025"]
CSTR --> ATT["csAttempt :454<br/>getTransport :515 · sendMsg :1138 · recvMsg :1162"]
CSTR -.retry.-> SR["shouldRetry :701<br/>throttle :764 · pushback :734 · jitter :776"]
end
subgraph indirection["registry / config"]
CFG["configSelector → MethodConfig<br/>stream.go:263-297"]
ENC["encoding registry<br/>GetCompressor/Codec"]
end
subgraph transport["internal/transport"]
PW["pickerWrapper.pick :105<br/>blockingCh"]
H2["http2Client.NewStream :756<br/>grpc-timeout :613"]
WR["transportStream.Write ← write :1106"]
GOR["goroutine loopyWriter :476 · reader :420"]
end
WIRE[("HTTP/2 wire — node lá")]
INV --> NCS
NCS --> CFG
NCSP --> ENC
ATT --> PW
ATT --> H2
ATT --> WR
WR --> GOR --> WIREMermaid source
flowchart LR
subgraph public_api["call.go — public API"]
IV["ClientConn.Invoke :29<br/>unaryInt chain :34"] --> INV["invoke :65"]
end
subgraph engine["stream.go — stream engine"]
NCS["newClientStream :213"] --> NCSP["newClientStreamWithParams :302"]
NCSP --> CSTR["clientStream<br/>withRetry :831 · SendMsg :954 · RecvMsg :1025"]
CSTR --> ATT["csAttempt :454<br/>getTransport :515 · sendMsg :1138 · recvMsg :1162"]
CSTR -.retry.-> SR["shouldRetry :701<br/>throttle :764 · pushback :734 · jitter :776"]
end
subgraph indirection["registry / config"]
CFG["configSelector → MethodConfig<br/>stream.go:263-297"]
ENC["encoding registry<br/>GetCompressor/Codec"]
end
subgraph transport["internal/transport"]
PW["pickerWrapper.pick :105<br/>blockingCh"]
H2["http2Client.NewStream :756<br/>grpc-timeout :613"]
WR["transportStream.Write ← write :1106"]
GOR["goroutine loopyWriter :476 · reader :420"]
end
WIRE[("HTTP/2 wire — node lá")]
INV --> NCS
NCS --> CFG
NCSP --> ENC
ATT --> PW
ATT --> H2
ATT --> WR
WR --> GOR --> WIRE| Tầng | Vị trí | Rule | File:line |
|---|---|---|---|
| Metadata | newClientStream | Key/value outgoing metadata phải hợp lệ → codes.Internal | stream.go:233-246 |
| Control plane | config selector | Status code từ control plane bị giới hạn danh sách gRFC A54, code lạ → codes.Internal | stream.go:266-273 |
| Deadline | newClientStreamWithParams | mc.Timeout (service config) ép context.WithTimeout cho toàn RPC | stream.go:313-318 |
| Size | SendMsg | payload sau nén > maxSendMessageSize → codes.ResourceExhausted | stream.go:990-992 |
| Size | recvMsg → recv | decode bị chặn bởi maxReceiveMessageSize | stream.go:1189 |
| Compression | newClientStreamWithParams | UseCompressor chỉ định compressor chưa đăng ký → codes.Internal | stream.go:353-360 |
| Compression | recvMsg | Peer nén response bằng thuật toán ngoài AcceptCompressors → codes.Internal | stream.go:1179-1181 |
| Cardinality | recvMsg | Unary không nhận được message nào (EOF + status OK) → codes.Internal "cardinality violation" | stream.go:1195-1197 |
| Cardinality | recvMsg | Unary nhận >1 message (recv lần 2 không phải EOF) → codes.Internal | stream.go:1225-1232 |
| Pick | pickerWrapper.pick | ctx hết hạn khi chờ picker → map đúng DeadlineExceeded/Canceled kèm lỗi balancer gần nhất | picker_wrapper.go:123-136 |
| Retry | shouldRetry | Chỉ retry code thuộc RetryableStatusCodes; throttle đếm fail; MaxAttempts chặn trần; pushback sai định dạng → abort | stream.go:757-768,734-747 |
| Lifecycle | SendMsg | Gọi sau CloseSend → codes.Internal "SendMsg called after CloseSend" | stream.go:965-967 |
Toàn bộ mục này là [AI suy luận — cần domain owner xác nhận] — với grpc-go, "domain owner" là gRFC/spec chính thức; chế độ đối chiếu PRD của skill có thể chạy trực tiếp với gRFC A6 (retry), A54 (control plane codes), A31 (config selector).
- Capability: cho phép application code thực hiện một unary RPC đồng bộ với semantics đầy đủ: per-method config từ service config (timeout/retry/size —
stream.go:263-297), interceptor chain (call.go:34-36), nén tùy chọn (stream.go:347-364), retry an toàn (chỉ khi stream chưa được server xử lý hoặc code retryable —stream.go:708-719,757-759). - Semantics chờ: RPC đầu tiên chờ resolver (
stream.go:249), pick block khi chưa có subchannel READY trừ khi failfast (picker_wrapper.go:100-104) — nghĩa là "gọi RPC" ngầm bao gồm "chờ hạ tầng sẵn sàng", không fail ngay. - Chuyển trạng thái attempt: tạo attempt → committed (khi RecvMsg bắt đầu —
stream.go:1037) hoặc retry (transparent/policy) → finished;onCommitbáo cho config selector (stream.go:281). - Ngưỡng/config chi phối: service config per-method (
MethodConfig),disableRetryDialOption (stream.go:721), retry throttler per-ClientConn (stream.go:386), env/DialOption về compressor và message size mặc định (stream.go:330-331). - Điểm cần xác nhận với spec: (1) hành vi trả
nilcủaSendMsgunary khi write lỗi có được document ở API contract không (comment nói "generated code requires it" —stream.go:1148-1151); (2) cardinality violation trảcodes.Internal— gRFC nào quy định code này thay vìUnimplemented?
| # | Vị trí | Archetype | Kịch bản: input/trạng thái → hậu quả | File:line |
|---|---|---|---|---|
| F1 | csAttempt.sendMsg | G1 lỗi hoãn sang chỗ khác | Conn đứt giữa lúc write: unary SendMsg trả nil (không phải lỗi!) — caller tưởng đã gửi xong; lỗi thật chỉ xuất hiện ở RecvMsg qua status của stream. Code đọc if err := cs.SendMsg(req); err != nil để "xử lý lỗi gửi" là vô nghĩa với unary | stream.go:1147-1155 |
| F2 | newClientStreamWithParams | G3 context leak (documented) | App gọi Invoke nhưng bỏ ngang không đọc kết quả: cancel của child ctx chỉ chạy khi RecvMsg lỗi / SendMsg lỗi / conn đóng — ctx (và timer nếu có timeout) sống lâu hơn cần thiết (issue #1818, comment ngay trong code) | stream.go:308-323 |
| F3 | SendMsg + retry | G5 / #5 retry giữ payload trong RAM | Message lớn + retry policy bật: mỗi op được buffer kèm payload (bufferForRetryLocked) tới khi commit — nhiều RPC lớn đồng thời retry → memory phình; throttler chỉ hạn chế tần suất retry, không hạn chế size | stream.go:1006-1012, stream.go:764 |
| F4 | pickerWrapper.pick | biến thể #2 — block vô hạn | Không set deadline + wait-for-ready (failfast=false) + balancer không bao giờ READY: pick block mãi trên blockingCh, RPC treo không lỗi — chỉ thoát khi ctx bị cancel từ ngoài | picker_wrapper.go:111-140,172-174 |
| F5 | shouldRetry pushback | dữ liệu đối tác điều khiển luồng | Server trả grpc-retry-pushback-ms sai định dạng hoặc nhiều giá trị → client abort retry và đếm như failure cho throttle — server cấu hình sai làm giảm retry của toàn client | stream.go:734-747 |
| F6 | transport goroutines | #2 fire-and-forget (ĐƯỢC XỬ LÝ) | Frame thực sự ghi bởi loopyWriter goroutine, không phải RPC goroutine; loopy chết vì lỗi non-IO → close conn ngay (:478-481), reader chết → readerErrCh (:420). Ghi nhận: cơ chế lành mạnh, không phải bug — nhưng khi debug "RPC treo" phải nhìn 2 goroutine này | internal/transport/http2_client.go:420,476-481 |
| F7 | recvMsg cardinality | guard chống server sai | Server stream trả 0 hoặc >1 message cho unary → client chặn bằng codes.Internal cardinality violation thay vì im lặng nhận sai — fail loud, đúng hướng | stream.go:1195-1197,1225-1232 |
| F8 | combine | G4 slice sharing (ĐÃ PHÒNG) | Comment giải thích rõ: dùng append trên slice còn capacity sẽ share phần tử giữa các call đồng thời → race; code cấp phát mới. Pattern đáng học cho code Go khác | call.go:40-53 |
Toàn bộ mục này là [AI suy luận — cần domain owner xác nhận].
- Credentials: per-call creds gắn qua CallOption vào
callHdr.Creds(stream.go:365-367); transport-level creds nằm ngoài phạm vi trace này (thuộc Dial path). Không thấy creds bị log trong nhánh đã trace. - Dữ liệu nhạy cảm: khi binarylog bật, toàn bộ header + message (materialized) được log (
stream.go:416-427,1013-1021,1038-1046) — môi trường có PII phải kiểm soát sink của binarylog. Stats handlers cũng nhận nguyên payload (stream.go:1156-1158,1211-1220) — plugin observability tự viết là điểm rò rỉ tiềm năng. - Retry/timeout: timeout per-method từ service config ép vào ctx (
stream.go:313-318) và truyền xuống wire qua headergrpc-timeout(internal/transport/http2_client.go:613) — server biết deadline của client. Retry có throttling per-ClientConn + server pushback + jitter 0.8-1.2 (stream.go:764,772-782) — thiết kế chống retry storm sẵn. - Idempotency: transparent retry chỉ khi stream Unprocessed (server chưa chạy handler —
stream.go:712-719) nên an toàn; policy retry theoRetryableStatusCodesdo người vận hành khai trong service config — khai code không idempotent (ví dụ retry trênUNKNOWN) là tự chuốc double-execution, thư viện không chặn hộ.
combine()kèm comment giải thích race — mẫu tốt về "comment nói lý do, không nói cái gì" (call.go:40-53).- TODO còn treo: check size theo
len(data)thay vì payload (stream.go:989); API merge metadata.MD vào outgoing context chưa có (stream.go:546-553). - Top-level
Invokedeprecated vẫn export cho tương thích (call.go:55-61). - Semantics
SendMsgunary (trả nil khi write lỗi) chỉ được giải thích bằng comment nội bộ (stream.go:1148-1151) — đáng được nêu trong doc comment public củaSendMsg. - Cardinality violation checks (
stream.go:1195-1197,1225-1232) là hardening tương đối mới — message lỗi rõ ràng, dễ grep. newClientStreamWithParamsdài (~120 dòng setup) nhưng tuyến tính, mỗi block có comment — đọc được; phần retry (withRetry/shouldRetry/buffer replay) là vùng phức tạp nhất, xứng đáng có diagram riêng nếu trace sâu (v2).
Mọi liên kết không lần ra được phải nằm ở đây — cấm đoán rồi trình bày như fact.
- Server-side leg (
server.goprocessUnaryRPC, nối bằng full method name): ngoài phạm vi trace này (client-side only theo golden flow user chọn). - Resolver/balancer nội bộ (
ccResolverWrapper,balancer_wrapper.go, quá trình sinh picker): chỉ trace đến điểm nốipickerWrapper.pick; đường resolver→balancer→picker update là nhánh riêng. - Transport flow control (controlBuf, loopyWriter internals, BDP, window update): node lá v1 — đã ghi nhận goroutine :420/:476 nhưng không mở hộp.
prepareMsg/recvinternals (rpc_util.goframing, header 5-byte): dùng như hộp đen qua call sitestream.go:973,1189; chưa Read file.- Dial path (
NewClient, resolver build lần đầu, idle manager exit-idle): điểm chạm duy nhất đã trace làwaitForResolvedAddrs(stream.go:249→clientconn.go:759— chưa Read thân hàm). - Nhánh streaming RPC (client/server streams,
CloseSend:1054-1067): khác semantics unary đáng kể — flow riêng.
| Ngày | Commit | Thay đổi so với lần trước |
|---|---|---|
| 2026-07-18 | grpc-go@7d870454 (master) | Lần trace đầu — golden-flow gate cho skill flow-trace sinh bởi flow-trace-genesis v0.1.0 |