@sema-agent/core 5.21.1 → 5.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/dist/agents/send-message-tool.js +6 -3
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +48 -6
- package/dist/brain/errors.d.ts +20 -0
- package/dist/brain/errors.js +40 -0
- package/dist/brain/retry.d.ts +16 -2
- package/dist/brain/retry.js +3 -2
- package/dist/brain/status-sink.d.ts +9 -2
- package/dist/brain/stream-engine.d.ts +22 -0
- package/dist/brain/stream-engine.js +41 -10
- package/dist/core/ask-class.d.ts +48 -0
- package/dist/core/ask-class.js +33 -0
- package/dist/core/checkpoint-store.d.ts +103 -10
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/governance-codes.d.ts +38 -0
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +101 -1
- package/dist/core/hooks.js +116 -14
- package/dist/core/locked-config.d.ts +7 -1
- package/dist/core/locked-config.js +2 -1
- package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
- package/dist/core/memory-engine/delegation-provenance.js +26 -0
- package/dist/core/memory-engine/engine.d.ts +94 -1
- package/dist/core/memory-engine/engine.js +329 -12
- package/dist/core/memory-engine/header-hints.d.ts +30 -0
- package/dist/core/memory-engine/header-hints.js +41 -0
- package/dist/core/memory-engine/index.d.ts +3 -2
- package/dist/core/memory-engine/index.js +3 -2
- package/dist/core/memory-engine/layout.d.ts +232 -0
- package/dist/core/memory-engine/layout.js +469 -1
- package/dist/core/memory-engine/tools.d.ts +30 -0
- package/dist/core/memory-engine/tools.js +108 -17
- package/dist/core/permission-rule-consent.d.ts +25 -9
- package/dist/core/permission-rule-consent.js +93 -21
- package/dist/core/permission-rule-model.d.ts +9 -1
- package/dist/core/permission-rule-model.js +2 -2
- package/dist/core/permission-rule-org.d.ts +195 -0
- package/dist/core/permission-rule-org.js +234 -0
- package/dist/core/permission-rule-store.d.ts +249 -6
- package/dist/core/permission-rule-store.js +314 -3
- package/dist/core/permission-rule-sync.d.ts +139 -0
- package/dist/core/permission-rule-sync.js +343 -0
- package/dist/core/runner/prepare-memory.js +35 -8
- package/dist/core/runner/prepare-task.d.ts +64 -3
- package/dist/core/runner/prepare-task.js +357 -32
- package/dist/core/runner/runtask.js +166 -6
- package/dist/core/shared-memory/contract.js +19 -4
- package/dist/core/shared-memory/normalize.d.ts +3 -1
- package/dist/core/shared-memory/tools.js +73 -17
- package/dist/core/shared-memory/types.d.ts +27 -1
- package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
- package/dist/core/task-notification.d.ts +5 -2
- package/dist/core/task-registry-agent.d.ts +1 -1
- package/dist/core/task-registry-agent.js +6 -2
- package/dist/core/task-registry-shared.d.ts +9 -2
- package/dist/core/task-registry.d.ts +9 -3
- package/dist/core/task-registry.js +2 -0
- package/dist/core/tool-policy.d.ts +155 -4
- package/dist/core/tool-policy.js +148 -10
- package/dist/core/tool-result-store.d.ts +9 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +79 -1
- package/dist/core/types.d.ts +94 -3
- package/dist/core/wiring-manifest.d.ts +16 -1
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.js +12 -5
- package/dist/orchestration/goal.d.ts +10 -0
- package/dist/orchestration/goal.js +6 -5
- package/dist/stores/file/adoption/adopt.d.ts +146 -0
- package/dist/stores/file/adoption/adopt.js +616 -0
- package/dist/stores/file/adoption/marker.d.ts +194 -0
- package/dist/stores/file/adoption/marker.js +198 -0
- package/dist/stores/file/background-agent-store.js +2 -0
- package/dist/stores/file/checkpoint-store.d.ts +4 -0
- package/dist/stores/file/checkpoint-store.js +3 -0
- package/dist/stores/file/file-snapshot-store.js +2 -0
- package/dist/stores/file/index.d.ts +2 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/memory-store.js +2 -0
- package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
- package/dist/stores/file/permission-rule-adopt.js +95 -0
- package/dist/stores/file/permission-rule-store.d.ts +80 -2
- package/dist/stores/file/permission-rule-store.js +189 -46
- package/dist/stores/file/session-policy-store.js +2 -0
- package/dist/stores/file/session-store.js +2 -0
- package/dist/stores/file/task-list-store.js +2 -0
- package/dist/stores/file/tool-result-store.js +2 -0
- package/dist/stores/file/usage-window-store.js +2 -0
- package/dist/stores/file/workflow-journal-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -10
- package/package.json +3 -2
|
@@ -273,6 +273,238 @@ export declare function recordRetrievedAccount(controlDir: string, ids: readonly
|
|
|
273
273
|
export declare function readRetrievedAccount(controlDir: string): Record<string, RetrievedAccountRow>;
|
|
274
274
|
/** S2-C — clear the fuse for files harvest just ACCEPTED (a committed write resets the strike count). */
|
|
275
275
|
export declare function clearScanFuse(controlDir: string, keys: Iterable<string>): void;
|
|
276
|
+
/** The write-ahead harvest lineage ledger (control plane, fail-closed). */
|
|
277
|
+
export declare const LINEAGE_FILE = "lineage.json";
|
|
278
|
+
/** The challenge generation-event ledger (control plane, fail-closed). */
|
|
279
|
+
export declare const CHALLENGES_FILE = "challenges.json";
|
|
280
|
+
/** Per-commit lineage AUDIT history (bookkeeping, fail-open, bounded rotation) — the load-bearing
|
|
281
|
+
* queries never read it; it exists so the deduped contribution set is not the only trace. */
|
|
282
|
+
export declare const LINEAGE_AUDIT_FILE = "lineage-audit.json";
|
|
283
|
+
/** Bound on retained audit rows (oldest rotated out first). */
|
|
284
|
+
export declare const LINEAGE_AUDIT_MAX_ROWS = 2048;
|
|
285
|
+
/** The challenged-history account (design/180 B-3: collect-only, fail-open bookkeeping —
|
|
286
|
+
* the mechanical shadow of "user saw a retrieved entry and corrected it in the same session"). */
|
|
287
|
+
export declare const CHALLENGED_HISTORY_FILE = "usage-challenged-history.json";
|
|
288
|
+
/** One committed contribution: session → entry, latest rev/at only (the load-bearing invariant is
|
|
289
|
+
* "which sessions contributed this id", not the per-commit history — that is the audit account). */
|
|
290
|
+
export interface LineageContribution {
|
|
291
|
+
lastRev: string;
|
|
292
|
+
lastAt: number;
|
|
293
|
+
}
|
|
294
|
+
/** One staged (pre-commit) row: an entry this transaction WOULD commit. */
|
|
295
|
+
export interface LineagePendingRow {
|
|
296
|
+
entryId: string;
|
|
297
|
+
rev: string;
|
|
298
|
+
}
|
|
299
|
+
/** One staged transaction: rows land BEFORE `applyPatches`; the commit credential lands after it
|
|
300
|
+
* succeeds; promotion consumes both. A pending txn without a credential is the crash window —
|
|
301
|
+
* it is NEVER auto-discarded (design/180 r5: the applyPatches-success→credential-write gap is
|
|
302
|
+
* undecidable on the pinned backend contract; the host adjudicates via the explicit API). */
|
|
303
|
+
export interface LineagePendingTxn {
|
|
304
|
+
sessionId: string;
|
|
305
|
+
at: number;
|
|
306
|
+
rows: LineagePendingRow[];
|
|
307
|
+
/** Present ⇔ `applyPatches` returned and its applied set was durably recorded. */
|
|
308
|
+
credential?: {
|
|
309
|
+
appliedIds: string[];
|
|
310
|
+
at: number;
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/** Stage this transaction's would-commit rows BEFORE `applyPatches` (write-ahead). Throws on any
|
|
314
|
+
* write/lock/corruption failure — the caller refuses the whole harvest (fail-closed). */
|
|
315
|
+
export declare function stageLineagePending(controlDir: string, txnId: string, sessionId: string, rows: readonly LineagePendingRow[], now: () => number): void;
|
|
316
|
+
/** Discard a staged transaction that never reached `applyPatches` (the pre-commit abort path —
|
|
317
|
+
* step ③ of the handshake, or a refused commit). Safe by construction: nothing was committed. */
|
|
318
|
+
export declare function discardLineagePending(controlDir: string, txnId: string): void;
|
|
319
|
+
/** Record the commit credential — `applyPatches` returned, these ids were APPLIED (non-delete ops).
|
|
320
|
+
* Written before promotion so a crash between the two converges by credential, never by reading
|
|
321
|
+
* the backend's current state back (design/180 r4: later syncs/updates rewrite that scene). */
|
|
322
|
+
export declare function recordLineageCredential(controlDir: string, txnId: string, appliedIds: readonly string[], now: () => number): void;
|
|
323
|
+
/** What one promotion moved into the committed set. */
|
|
324
|
+
export interface LineagePromotion {
|
|
325
|
+
entryId: string;
|
|
326
|
+
sessionId: string;
|
|
327
|
+
rev: string;
|
|
328
|
+
}
|
|
329
|
+
/** Promote a CREDENTIALED pending transaction: rows named by the credential's applied set join the
|
|
330
|
+
* committed contribution set (deduped by (entryId, sessionId) — a repeat pair refreshes
|
|
331
|
+
* lastRev/lastAt, never grows the set); rows NOT in the applied set (conflicted/refused ops) are
|
|
332
|
+
* dropped without ever becoming ancestry. Throws when the txn is unknown or uncredentialed. */
|
|
333
|
+
export declare function promoteLineagePending(controlDir: string, txnId: string, now: () => number): LineagePromotion[];
|
|
334
|
+
/** The host adjudication for the UNDECIDABLE crash window (a pending txn with no credential —
|
|
335
|
+
* design/180 r5: never auto-discarded). `"promote"` = the host verified the commit landed (all
|
|
336
|
+
* staged rows join the committed set); `"discard"` = the host verified it did not. Idempotent on
|
|
337
|
+
* an unknown txn. Also accepts a credentialed txn (then `"promote"` follows the credential). */
|
|
338
|
+
export declare function adjudicateLineagePending(controlDir: string, txnId: string, action: "promote" | "discard", now: () => number): LineagePromotion[];
|
|
339
|
+
/** Reconciliation (harvest-opening / host-triggered): promote every CREDENTIALED orphan pending
|
|
340
|
+
* txn; report the uncredentialed ones (their latch stays until the host adjudicates). */
|
|
341
|
+
export declare function reconcileLineage(controlDir: string, now: () => number): {
|
|
342
|
+
promoted: LineagePromotion[];
|
|
343
|
+
undecidable: Array<{
|
|
344
|
+
txnId: string;
|
|
345
|
+
sessionId: string;
|
|
346
|
+
entryIds: string[];
|
|
347
|
+
}>;
|
|
348
|
+
};
|
|
349
|
+
/** The DIRTY-LATCH set: every entry id named by ANY pending row (credentialed or not). While an id
|
|
350
|
+
* is latched, its model-visible read faces (index mechanical rows / memory_search / memory_get)
|
|
351
|
+
* refuse — "the account is not settled, the content does not go on the table". Throws on a
|
|
352
|
+
* corrupt ledger (integrity unknown ≠ no latch). */
|
|
353
|
+
export declare function lineageLatchedIds(controlDir: string): Set<string>;
|
|
354
|
+
/** Every committed contribution of `sessionId` (the pollution-retroaction sweep input). Throws on
|
|
355
|
+
* a corrupt ledger. */
|
|
356
|
+
export declare function lineageContributionsOfSession(controlDir: string, sessionId: string): Array<{
|
|
357
|
+
entryId: string;
|
|
358
|
+
lastRev: string;
|
|
359
|
+
}>;
|
|
360
|
+
/** Drop an entry's lineage rows once the entry is DELETED (an applied tombstone) — space hygiene
|
|
361
|
+
* only, taken at the harvest that saw the applied delete (design/180 §10). */
|
|
362
|
+
export declare function clearLineageForEntries(controlDir: string, entryIds: readonly string[]): void;
|
|
363
|
+
/** Full-ledger read (tests / host observability). Throws on corruption. */
|
|
364
|
+
export declare function readLineageRecord(controlDir: string): {
|
|
365
|
+
committed: Record<string, Record<string, LineageContribution>>;
|
|
366
|
+
pending: Record<string, LineagePendingTxn>;
|
|
367
|
+
};
|
|
368
|
+
/** Fail-open per-commit AUDIT history (bounded rotation) — never consulted by the load-bearing
|
|
369
|
+
* queries; a broken audit account degrades silently, exactly like the retrieved account. */
|
|
370
|
+
export declare function appendLineageAudit(controlDir: string, rows: ReadonlyArray<{
|
|
371
|
+
entryId: string;
|
|
372
|
+
sessionId: string;
|
|
373
|
+
rev: string;
|
|
374
|
+
at: number;
|
|
375
|
+
}>): void;
|
|
376
|
+
/** One challenge/resolve event. Append-only; the exclusion predicate is DERIVED ("any unresolved
|
|
377
|
+
* challenge generation exists"), never stored as a mutable flag. */
|
|
378
|
+
export interface ChallengeEvent {
|
|
379
|
+
/** Stable identity for idempotent replay: the automatic arm derives it from the pollution event
|
|
380
|
+
* anchor (`pollution:<sessionId>:<markedAt>:<entryId>`); the host arm derives it from the
|
|
381
|
+
* caller's requestId (`<requestId>:<entryId>`). Same eventId replayed = same generation (a lost
|
|
382
|
+
* ack retried never opens a second generation); a DIFFERENT eventId with the same reason is a
|
|
383
|
+
* genuinely new event and opens a new generation (no immunity hole). */
|
|
384
|
+
eventId: string;
|
|
385
|
+
entryId: string;
|
|
386
|
+
kind: "challenge" | "resolve";
|
|
387
|
+
/** The generation this event opened (challenge) or closed (resolve). */
|
|
388
|
+
generation: number;
|
|
389
|
+
at: number;
|
|
390
|
+
reason: string;
|
|
391
|
+
/** Evidence grade — v2 mints only "S" (structural) events. */
|
|
392
|
+
grade?: "S";
|
|
393
|
+
/** Audit record only (the exclusion predicate keys the entry ID, never the rev). */
|
|
394
|
+
challengedRev?: string;
|
|
395
|
+
}
|
|
396
|
+
/** One challenge assignment {@link appendChallengeEvents} returns. `replayed` = the eventId already
|
|
397
|
+
* existed (idempotent retry — the ORIGINAL generation is returned, no new one opens). */
|
|
398
|
+
export interface ChallengeAssignment {
|
|
399
|
+
entryId: string;
|
|
400
|
+
eventId: string;
|
|
401
|
+
generation: number;
|
|
402
|
+
at: number;
|
|
403
|
+
replayed: boolean;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* design/180 §10 — the challenge ledger's SIZE CALIBRATION.
|
|
407
|
+
*
|
|
408
|
+
* The ledger is append-only and cannot be rotated: the exclusion predicate is DERIVED from the events
|
|
409
|
+
* ("a challenge generation with no matching resolve"), so dropping the oldest rows would silently
|
|
410
|
+
* un-withhold their entries — taint laundering, the exact failure the fail-closed family exists to
|
|
411
|
+
* prevent. The bound is therefore a DISCLOSURE threshold, not a cap: crossing it changes nothing about
|
|
412
|
+
* what the ledger holds, it tells the deployment the account needs attention.
|
|
413
|
+
*
|
|
414
|
+
* Calibrated against what actually degrades, which is not disk: every append REWRITES the whole file
|
|
415
|
+
* under the strict lock, and every model-visible read path ({@link challengedEntryIds}, called on each
|
|
416
|
+
* index rebuild and each memory tool call) PARSES it whole. At ~200 bytes per event, 10k events is a
|
|
417
|
+
* ~2 MB parse on a read path that runs per tool call — the point where "an audit account nobody looks
|
|
418
|
+
* at" becomes a latency the session feels. A deployment reaching it should resolve the settled
|
|
419
|
+
* generations (they stay in the file, and that is the point) or archive the account deliberately.
|
|
420
|
+
*/
|
|
421
|
+
export declare const CHALLENGE_LEDGER_MAX_EVENTS = 10000;
|
|
422
|
+
/** What {@link appendChallengeEvents} returns: the per-request assignments plus the ledger's size
|
|
423
|
+
* AFTER the append — the caller owns disclosure (this module has no sink), and reading the size here
|
|
424
|
+
* costs nothing because the record is already in hand under the lock. */
|
|
425
|
+
export interface ChallengeAppendResult {
|
|
426
|
+
assignments: ChallengeAssignment[];
|
|
427
|
+
/** Total events in the ledger after this append, or `undefined` when nothing was appended — the
|
|
428
|
+
* empty-request fast path takes no lock and reads nothing, so it has no size to report and must not
|
|
429
|
+
* report a made-up `0` (a caller comparing that against the bound would read "tiny", not "unknown"). */
|
|
430
|
+
eventCount: number | undefined;
|
|
431
|
+
}
|
|
432
|
+
/** Append S-grade challenge events (idempotent per eventId, generation allocation inside the strict
|
|
433
|
+
* lock). Every NEW eventId opens a NEW generation for its entry — a resolve of an earlier
|
|
434
|
+
* generation never immunizes the entry against later pollution (design/180 R-8). */
|
|
435
|
+
export declare function appendChallengeEvents(controlDir: string, events: ReadonlyArray<{
|
|
436
|
+
eventId: string;
|
|
437
|
+
entryId: string;
|
|
438
|
+
reason: string;
|
|
439
|
+
challengedRev?: string;
|
|
440
|
+
}>, now: () => number): ChallengeAppendResult;
|
|
441
|
+
/** The trusted resolve: closes EXACTLY the named generation (a later challenge's generation stays
|
|
442
|
+
* open — resolves never blanket-clear). Idempotent: resolving an already-resolved generation is a
|
|
443
|
+
* no-op. `resolved` is false ⇔ no such open generation existed.
|
|
444
|
+
*
|
|
445
|
+
* `eventCount` rides along for the same reason it does on {@link appendChallengeEvents}: a resolve
|
|
446
|
+
* APPENDS an event too, so it crosses the size bound exactly as a challenge does (independent
|
|
447
|
+
* adversarial review, verified — hanging the disclosure off the challenge writer alone leaves a ledger
|
|
448
|
+
* that crossed the bound on a resolve silent until the next challenge, which may never come).
|
|
449
|
+
* `undefined` when nothing was appended. */
|
|
450
|
+
export declare function resolveChallengeEvent(controlDir: string, entryId: string, generation: number, reason: string, now: () => number, requestId?: string): {
|
|
451
|
+
resolved: boolean;
|
|
452
|
+
eventCount: number | undefined;
|
|
453
|
+
};
|
|
454
|
+
/** The exclusion predicate's input: entry id → its LATEST unresolved challenge generation. An entry
|
|
455
|
+
* appears iff at least one challenge generation has no matching resolve. Throws on corruption —
|
|
456
|
+
* consumers refuse service (integrity unknown ≠ clean). */
|
|
457
|
+
export declare function challengedEntryIds(controlDir: string): Map<string, {
|
|
458
|
+
generation: number;
|
|
459
|
+
at: number;
|
|
460
|
+
}>;
|
|
461
|
+
/** Full event read (host audit / tests). Throws on corruption. */
|
|
462
|
+
export declare function readChallengeEvents(controlDir: string): ChallengeEvent[];
|
|
463
|
+
/** Which fail-closed control-plane ledger a rebuild targets. */
|
|
464
|
+
export type StrictControlPlaneLedger = "lineage" | "challenges";
|
|
465
|
+
/** What one rebuild did — the deployment's record of a taint-bearing account it chose to reset. */
|
|
466
|
+
export interface ControlPlaneRebuildReceipt {
|
|
467
|
+
ledger: StrictControlPlaneLedger;
|
|
468
|
+
/** The corrupt file (and its journal, when one existed) moved aside — never deleted. */
|
|
469
|
+
quarantinedTo: string[];
|
|
470
|
+
at: number;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* design/180 §10 — rebuild a CORRUPT fail-closed ledger, explicitly.
|
|
474
|
+
*
|
|
475
|
+
* The fail-closed family refuses service on corruption precisely because a silent "start fresh" would
|
|
476
|
+
* LAUNDER taint (a challenged entry re-enters the prompt; a polluted session's contributions become
|
|
477
|
+
* untraceable). That refusal is permanent by design — which left a deployment whose disk really did
|
|
478
|
+
* corrupt with no way back other than deleting engine files by hand. This is that way back, and its
|
|
479
|
+
* whole shape is "never automatic":
|
|
480
|
+
* · the caller names the ledger — nothing in the engine calls this on its own;
|
|
481
|
+
* · a HEALTHY ledger is REFUSED (`memory.control_plane_not_corrupt`) — a rebuild is only ever the
|
|
482
|
+
* answer to a corruption that is already blocking every read, never a way to clear an account;
|
|
483
|
+
* · the corrupt bytes are QUARANTINED beside the file, never deleted: what the ledger held is
|
|
484
|
+
* exactly the evidence someone will want after a laundering decision;
|
|
485
|
+
* · the caller (the engine's host API) announces the rebuild — a reset of this account changes what
|
|
486
|
+
* the model may see, so it is not a silent repair.
|
|
487
|
+
*/
|
|
488
|
+
/** Lock-less probe: does this fail-closed ledger currently REFUSE reads? The rebuild's own check runs
|
|
489
|
+
* again under the lock (this one is for a caller that must decide something — announce, refuse — before
|
|
490
|
+
* taking the lock). `false` for a ledger that reads cleanly, including an absent one (genuinely empty). */
|
|
491
|
+
export declare function isStrictControlPlaneLedgerCorrupt(controlDir: string, ledger: StrictControlPlaneLedger): boolean;
|
|
492
|
+
export declare function rebuildStrictControlPlaneLedger(controlDir: string, ledger: StrictControlPlaneLedger, now: () => number): ControlPlaneRebuildReceipt;
|
|
493
|
+
/** One challenged-history row: how often a retrieved-then-corrected event hit this entry. */
|
|
494
|
+
export interface ChallengedHistoryRow {
|
|
495
|
+
count: number;
|
|
496
|
+
lastAt: number;
|
|
497
|
+
lastOp: "update" | "tombstone";
|
|
498
|
+
}
|
|
499
|
+
/** Record mechanical correction shadows (same-session retrieved → update/tombstone committed).
|
|
500
|
+
* Collect-only by contract: NOTHING in the engine reads this back into any decision (v3 candidate
|
|
501
|
+
* evidence only — design/178 ③-1 三级账 discipline). Throws propagate; callers treat fail-open. */
|
|
502
|
+
export declare function recordChallengedHistory(controlDir: string, rows: ReadonlyArray<{
|
|
503
|
+
entryId: string;
|
|
504
|
+
op: "update" | "tombstone";
|
|
505
|
+
}>, now: () => number): void;
|
|
506
|
+
/** Journal-aware read (observability/tests only — no engine consumer exists, on purpose). */
|
|
507
|
+
export declare function readChallengedHistory(controlDir: string): Record<string, ChallengedHistoryRow>;
|
|
276
508
|
/**
|
|
277
509
|
* REF-C6 — write EVERY byte of `data` to `fd`, looping until the OS has taken all of them.
|
|
278
510
|
*
|