Skip to content

Let any sink hold files: FileStoreSink protocol and a chunked filesystem - #112

Open
Vedaant-J wants to merge 47 commits into
mainfrom
vedaant/mlops-filestore-sink
Open

Vedaant-J wants to merge 47 commits into
mainfrom
vedaant/mlops-filestore-sink

Conversation

@Vedaant-J

@Vedaant-J Vedaant-J commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stack: main (#53 merged) → #112 → #114 → #50 → #60 → #64 → #115. Merge bottom to top.

Summary

Prediction exports and model bundles can only go to S3, GCS, ADLS or local paths: the engine checks the connector type. Databricks volumes and Snowflake stages hold files too, but one upload is capped (5 GiB on volumes) or has no multipart. This PR lets any sink say which of its paths are files, and gives such stores one shared filesystem:

  • kumoapi.plugin.FileStoreSink: an optional protocol, holds_files(connector, path) (from the path alone) and filesystem(connector, path) (a pyarrow.fs.FileSystem).
  • kumo_core.chunked_fs.ChunkedFileHandler: a pyarrow filesystem over five store calls (size, list, put, ranged read, remove). A file up to part_bytes is one plain object; a larger one is stored as parts under <name>.chunks/<generation>/ and committed by an empty _SUCCESS written last.
  • kumo_core.object_store_sink.FilesystemSink: the existing object-store writers over any sink's filesystem(). Bundles upload _provenance.json last.
  • kumo_core.object_store_sink.FileStoreRouting: a mixin that sends a sink's file paths to FilesystemSink and its other paths to its own table methods.
  • ObjectStoreSinkBackend.preflight_write_files: an export into an occupied output directory is refused before scoring, on every object store.

No volume or stage sink ships here; #114 and #115 add them.

Usage

class StageSink:                                    # an existing table sink
    def holds_files(self, connector, path):
        return path.startswith('@')
    def filesystem(self, connector, path):
        return PyFileSystem(StageHandler(connector)), path.rstrip('/')

PredictionOutputConfig(connector, output_paths={"predictions": "@stage/kumo/scores/"})
trainer.fit(..., export_config=ModelOutputConfig(connector, path="@stage/kumo/models/m1"))
Trainer.load_from_connector(connector, path="@stage/kumo/models/m1")

Back-compat

Situation Behaviour
S3, GCS, ADLS or local publish and fetch Unchanged (ObjectStoreSinkBackend holds every path).
output_paths on a sink that does not hold the path ValueError, as before; the message now mentions volume and stage paths.
Sink whose package extra is not installed The install hint is raised, not the file-destination error.
Export into an output directory that already holds files (S3, GCS, ADLS or a sink's file store) Refused before scoring, whatever the write mode, with the same Output directory ... already contains data from another export error that was raised after scoring.
A sink without FileStoreSink A table destination, as before.
A chunked file read while it is rewritten The previous file until the new _SUCCESS lands.
A write that fails part way Nothing committed; a stream that supports abort is aborted; the previous file stays readable; the error is raised.
A .chunks/ file read by Spark or an older engine A directory of parts; files up to part_bytes are plain objects.

Testing

  • kumo-core/tests/test_chunked_fs.py: empty, sub-part, one-part and multi-part files; reads across part boundaries; newest committed generation wins; failed writes keep the old file and abort the stream; listings, deletes, streamed writes, spool location; concurrent rewrites.
  • kumo-engine/test/test_filestore_sink.py: output configs, a bundle round trip, trainer publish and load, and prediction export, the occupied-directory refusal before scoring, and a file-only sink refusing a table through a filesystem-only sink over an in-memory store.

Change map: see the kumo-pr-change-map comment.

🤖 Generated with Claude Code

@copy-pr-bot

copy-pr-bot Bot commented Oct 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 24, 2026 17:17 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Superseded by the change map below.

@Vedaant-J

Vedaant-J commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Change map

File Function / scope What changed Why
packages/kumo-api/kumoapi/plugin.py FileStoreSink New optional protocol: holds_files, filesystem Lets any sink hold files for some paths
packages/kumo-core/src/kumo_core/artifacts.py PROVENANCE_FILE New constant _provenance.json One name for every artifact's manifest
packages/kumo-core/src/kumo_core/chunked_fs.py ChunkedFileHandler, set_spool_dir, spool_root New pyarrow filesystem over put/list/read/remove; large files as committed parts; a commit drops only older committed generations and reads skip incomplete ones Stores that cap object size or lack multipart
packages/kumo-core/src/kumo_core/object_store_sink.py holds_files, filesystem, preflight_write_files, FilesystemSink, FileStoreRouting, open_sized, upload_file, upload_bundle Object-store writers over any sink's filesystem(); occupied output directory refused before scoring on every store; manifest uploaded last Reuse export and bundle code for volumes and stages
packages/kumo-engine/kumo/engine/connector/__init__.py is_file_destination, file_store Pick file store or table per path Replaces the connector-type check
packages/kumo-engine/kumo/engine/trainer/output_config.py PredictionOutputConfig, TrainingTableOutputConfig Accept any path a sink holds; new writer() Exports to volume and stage paths
packages/kumo-engine/kumo/engine/trainer/trainer.py _require_publisher, _preflight, fit, predict, PredictionResult._export Publisher check, preflight and export writer come from the sink protocols Publish and export by file path on any sink
packages/kumo-engine/kumo/engine/_scratch.py _spool_parts Chunked writers spool under <scratch>/spool Follows kumo.configure(scratch_dir=)
packages/kumo-core/tests/test_chunked_fs.py new tests Layouts, boundary reads, failed writes and opens, generations, overlapping writers, concurrency Covers the chunked filesystem contract
packages/kumo-engine/test/test_filestore_sink.py new tests Configs, bundles, trainer, export over a filesystem-only sink End to end without a real store
packages/kumo-engine/test/test_prediction_completion.py fake sink Fake file sink implements FileStoreSink Export path now asks the sink
tests/public/Dockerfile.dockerignore public image allowlist Ships chunked_fs.py object_store_sink imports it
packages/kumo-adls/tests/test_native_output.py ADLS sink tests Copy through the shared upload_file Follows the upload helper's move
packages/kumo-engine/test/test_adls_connector.py ADLS connector test An ADLS Gen2 root is a file destination and a bundle round-trips through it ADLS artifact roots use the shared file-store path

Vedaant-J added a commit that referenced this pull request Oct 24, 2026
…cope

Snapshot export took (connector, path), gated on object-store sinks,
and was the only place a snapshot reached durable storage.

- GraphSnapshot.export(SnapshotOutputConfig(connector, path)), in the
  style of the other output configs; any sink that holds files at the
  path (FileStoreSink, #112) can take it, and every upload tells a
  chunked store its size so files up to one part stay plain.
- Graph.snapshot(mlops=): with MLOps on, the snapshot is exported under
  <root>/snapshots/ unless a complete export of the same format and id
  is there; failures warn.
- ConcurrentExportError is a RuntimeError (no retry role).
  GraphSnapshot.graph_key/data_version are private. The README section
  moves to docs/mlops.md.

Verified: test_snapshot_export.py (30: config form, a filesystem-only
chunked sink round trip, stored once in a block, existing export,
retry and repair cases), snapshot manifest and training-table suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J added this pull request to stack #113 October 24, 2026 19:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Other comments: 2
packages/kumo-core/src/kumo_core/chunked_fs.py: 2

104-114: Minor · Stability & Availability · Quick win

Fix the stale-layout race between _layout and _forget.

_layout computes a layout without a lock and then stores it in the cache. A writer can finish and call _forget(path) between the compute step and the store step. The reader then stores the old layout after the invalidation.

Nothing removes that entry afterward. The _Reader uses the cached sizes. If the new file is larger, a read returns truncated data without an error. If the sizes differ in another way, the read raises OSError.

The race needs a reader and a writer on the same path in one process. test_layout_cache_survives_concurrent_opens_and_writes does not cover it. In that test, each thread reads and writes only its own file.

Use an invalidation counter. Store the layout only if no _forget call ran during the compute step.

Proposed fix
     def _layout(self, path: str) -> Optional[List[Tuple[str, int]]]:
         cache = self.__dict__.setdefault('_layouts', {})
         layout = cache.get(path)
         if layout is not None:
             return layout
+        epoch = self.__dict__.get('_epoch', 0)
         size = self._size(path)
         layout = ([(path, size)] if size is not None else _logical_parts(
             self._list(path + CHUNKS), path + CHUNKS))
-        if layout is not None:
+        if layout is not None and self.__dict__.get('_epoch', 0) == epoch:
             cache[path] = layout
         return layout

In _forget, increment the counter before it removes entries:

        self.__dict__['_epoch'] = self.__dict__.get('_epoch', 0) + 1

If you need exact atomicity across threads, guard the counter update and the cache store with one threading.Lock.

Add a test in which one thread writes a path while other threads read the same path.


321-341: Minor · Data Integrity & Integration · Quick win

Guard failed _Writer construction from finalization.

When _open_part() fails after _clear(), _Writer remains partially initialized. io.RawIOBase finalization calls _Writer.close(), whose final branch uploads the empty spool as the plain object. _clear() has already removed the old chunked representation.

super().close() directly calls the inherited close method and does not re-enter _Writer.close(). Keep the resource setup inside the guard:

Suggested fix
-        self._dir = tempfile.mkdtemp(prefix='kumo-chunked-',
-                                     dir=spool_root(handler))
-        self._spool = open(os.path.join(self._dir, 'spool'), 'wb')
-        self._filled = 0
-        self._parts = 0
-        self._written = 0
-        self._part: Optional[BinaryIO] = None
-        self._streams = type(handler)._stream is not ChunkedFileHandler._stream
-        self._limit = (min(handler.plain_bytes, handler.part_bytes)
-                       if self._streams else handler.part_bytes)
-        self._plain: Optional[BinaryIO] = None
-        if self._streams and size is not None:
-            if size <= handler.part_bytes:
-                self._plain = handler._stream(path)
-            else:
-                self._clear()
-                self._part = self._open_part()
+        directory: Optional[str] = None
+        spool: Optional[BinaryIO] = None
+        try:
+            directory = tempfile.mkdtemp(prefix='kumo-chunked-',
+                                         dir=spool_root(handler))
+            self._dir = directory
+            spool = open(os.path.join(directory, 'spool'), 'wb')
+            self._spool = spool
+            self._filled = 0
+            self._parts = 0
+            self._written = 0
+            self._part: Optional[BinaryIO] = None
+            self._streams = type(handler)._stream is not ChunkedFileHandler._stream
+            self._limit = (min(handler.plain_bytes, handler.part_bytes)
+                           if self._streams else handler.part_bytes)
+            self._plain: Optional[BinaryIO] = None
+            if self._streams and size is not None:
+                if size <= handler.part_bytes:
+                    self._plain = handler._stream(path)
+                else:
+                    self._clear()
+                    self._part = self._open_part()
+        except BaseException:
+            if spool is not None:
+                spool.close()
+            if directory is not None:
+                shutil.rmtree(directory, ignore_errors=True)
+            super().close()
+            raise

This prevents the empty-object replacement. It does not restore data already removed by _clear().


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 893be81 and d2153fe.

Files selected for processing: 10
  • packages/kumo-api/kumoapi/plugin.py
  • packages/kumo-core/src/kumo_core/artifacts.py
  • packages/kumo-core/src/kumo_core/chunked_fs.py
  • packages/kumo-core/src/kumo_core/object_store_sink.py
  • packages/kumo-core/tests/test_chunked_fs.py
  • packages/kumo-engine/kumo/engine/_scratch.py
  • packages/kumo-engine/kumo/engine/connector/__init__.py
  • packages/kumo-engine/kumo/engine/trainer/output_config.py
  • packages/kumo-engine/test/test_filestore_sink.py
  • packages/kumo-engine/test/test_prediction_completion.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 6 remain after this review.

Vedaant-J and others added 3 commits October 28, 2026 16:00
fit() read test_df.parquet from best_trainer.log_dir, which is whichever
refit stage ran last, not necessarily the model the bundle publishes. With
refit_full the published model has seen the test split, so its holdout is
meaningless.

Read the holdout from the published model's log dir (Trainer.published_fit_mode,
also used to pick the prediction model) and return none for refit_full.
get_log_dir takes an optional fit_mode override.

Verified: test_holdout_df (fit / refit_trainval with explain / refit_full),
test_training_explanations, test_semantic_carry_forward: 35 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Snapshot exports, model bundles and export manifests could only go to
object stores: the engine checked for ObjectStoreSinkBackend, and
output configs accepted output_paths only for S3, GCS, ADLS and local
connectors. A store that is not an object store (a Databricks volume, a
Snowflake stage) had no way in, even with a pyarrow filesystem to offer.

kumoapi.plugin.FileStoreSink is an optional sink capability:
holds_files(connector, path) says from the path alone whether it is a
file location rather than a table, and filesystem(connector, path)
returns the pyarrow filesystem. ObjectStoreSinkBackend implements it.
kumo.engine.connector.is_file_destination/file_store are the engine's
one file-vs-table gate; PredictionOutputConfig(output_paths=) and the
TrainingTableOutputConfig directory slash use it, so object stores
behave as before. kumo_core gains FilesystemSink (the object-store file
writers over any filesystem()), upload_bundle/download_bundle (the
manifest file last), open_sized (tells a chunked filesystem the size)
and PROVENANCE_FILE. The chunked filesystem streams a sized file of up
to one part as one plain object, writes frames through a local spool so
they stay plain too, spools under the engine scratch directory read at
each write, and reads parts into the caller's buffer with one copy.

Verified: test_chunked_fs.py (26), test_filestore_sink.py (protocol,
output configs, bundles and Parquet over a filesystem-only sink), file
connector, completion, preflight, registry and kumo-core export suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_artifacts.py's fake trainer borrows Trainer methods; select_fit_mode
now calls published_fit_mode, so the fake needs it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-filestore-sink branch from db74c6c to fc4d709 Compare October 28, 2026 16:25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests November 1, 2026 01:48 — with GitHub Actions Inactive
Vedaant-J added a commit that referenced this pull request Nov 1, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vedaant-J added a commit that referenced this pull request Nov 1, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vedaant-J and others added 5 commits November 1, 2026 02:51
The refit_full case now also explains, as in the upper PRs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation

The preflight moves from FilesystemSink to ObjectStoreSinkBackend, so S3, GCS and ABFS outputs are refused before scoring too, and a conflict found while writing names the same full location.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd predict

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests November 1, 2026 02:56 — with GitHub Actions Inactive
Vedaant-J added a commit that referenced this pull request Nov 1, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vedaant-J added a commit that referenced this pull request Nov 1, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J removed this pull request from stack #118 November 2, 2026 18:00
@Vedaant-J
Vedaant-J added this pull request to stack #155 November 2, 2026 18:00
Base automatically changed from vedaant/mlops-holdout-fit-mode to main November 2, 2026 18:55
Vedaant-J added a commit that referenced this pull request Nov 2, 2026
**Series:** kumo-engine MLOps series: every snapshot, training table,
model and prediction run carries a self-contained _provenance.json;
`with kumo.mlops(...)` stores each artifact under one artifact root,
tracks fits and prediction runs in MLflow or Snowflake and registers
models; volumes and stages can hold files; prediction runs are monitored
for drift and realized performance.
**Stack:** **#53** → #112 → #114 → #50 → #60 → #64 → #115.  
**Merge order:** one linear stack; merge bottom to top (#53 first)

## Summary

This PR makes **`TrainingResult.holdout_path` the holdout of the model
that was published, or `None` when that model has no holdout**. The
answer no longer depends on whether explanations are on.

The holdout is the test-split predictions `fit()` writes to
`test_df.parquet`. `holdout_path` points at that file and `holdout_df()`
reads it. Teams use it to check the calibration and score distribution
of the model they are about to ship.

One fit can train up to three models from the best trial, and the bundle
ships the one trained on the most data:

| Fit mode | Trained on | Has a holdout |
|---|---|---|
| `fit` | train | yes |
| `refit_trainval` (`training_job.refit_trainval`) | train + val | yes |
| `refit_full` (`training_job.refit_full`) | train + val + test | no: it
saw the test split |

## Usage

```python
plan = ModelPlan()
plan.training_job.refit_trainval = True
result = kumo.Trainer(model_plan=plan).fit(training_table, snapshot=snapshot)

result.holdout_path   # .../version_1_refit_trainval/test_df.parquet: the shipped model's
result.holdout_df()   # its test-split predictions
```

With `refit_full` on, `holdout_path` is `None`, `holdout_df()` raises
`FileNotFoundError`, and the log says: `The published model is refit on
the full data, including the test split, so it has no holdout
predictions.`

## Production questions answered

- Are the holdout predictions I am checking from the model that shipped?
Yes, always.
- Does turning explanations on or off change which holdout I get? No.
- Why is there no holdout for my `refit_full` model? It trained on the
test split; the log says so.

## How it works

`fit()` asks kumo-core which fit mode the bundle publishes
(`Trainer.published_fit_mode()`: `refit_full`, else `refit_trainval`,
else `fit`) and reads `test_df.parquet` only from that model's run
directory. For `refit_full` it returns `None`. Prediction and the
holdout lookup share this one definition of the published model.

Run directory of a CPU fit on a small synthetic graph (`users`, `items`,
`orders`) with both refits on; the best trial is `version_1`:

```
<scratch>/trainers/<trainer>/runs/<run>/
  published-model/                     # the bundle; holds the refit_full model
  models/<run>/automl/
    version_0/  version_2/  version_3/  # other trials
    version_1/                         # best trial, fit mode 'fit'
      test_df.parquet  test_df_meta.json  test_result.csv  checkpoint/  ...
    version_1_refit_trainval/          # train + val, evaluated on test
      test_df.parquet  test_df_meta.json  test_result.csv  checkpoint/  ...
    version_1_refit_full/              # train + val + test, never evaluated
      checkpoint/  tensorboard/  train_progress.json  train_result.json
```

Which file `holdout_path` returns, before and after:

| refit_trainval | refit_full | explain | before | after |
|---|---|---|---|---|
| no | no | off/on | `version_N/` | `version_N/` |
| yes | no | off/on | `version_N_refit_trainval/` |
`version_N_refit_trainval/` |
| no | yes | off | `None` | `None` |
| no | yes | on | `version_N/` (not the shipped model) | `None` |
| yes | yes | off | `None` | `None` |
| yes | yes | on | `version_N_refit_trainval/` (not the shipped model) |
`None` |

Before, the lookup followed whichever fit mode the trainer object was
last left in, and `explain()` changed that mode as a side effect.
kumo-core gains `Trainer.published_fit_mode()` and an optional
`fit_mode` argument on `Trainer.get_log_dir`, so the lookup reads
another mode's directory without changing the trainer or creating empty
directories.

## Back-compat

Only the choice of file changes. No fields, checks or errors are added,
and the run directory layout is unchanged.

| Situation | Behaviour |
|---|---|
| No refit, or `refit_trainval` only | Same `holdout_path` as before,
explanations on or off. |
| `refit_full`, explanations off | `None`, as before. |
| `refit_full`, explanations on | `None`. It used to return an earlier
model's holdout without saying so. |
| No holdout written (e.g. ranking tasks) | `None`, as before. |
| Result of `Trainer.load()` | `None`, as before. |

## Testing

- `test_holdout_df.py`: real CPU fits with no refit (file under
`version_N/`), `refit_trainval` with explanations on (under
`version_N_refit_trainval/`), and both refits (`None`); the holdout is
never inside the bundle.
- `test_training_explanations.py`, `test_semantic_carry_forward.py`:
fake trainers answer `published_fit_mode()` and
`get_log_dir(fit_mode=)`.
- kumo-core `test_artifacts.py`, training and checkpoint suites pass
with the new `get_log_dir` argument.
- The three engine files above: 35 passed on CPU.

## Out of scope

- A holdout for a `refit_full` model. The `fit` and `refit_trainval`
test predictions stay in the run directory shown above; they are not
returned as the shipped model's.

Change map: see the `kumo-pr-change-map` comment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests November 2, 2026 19:32 — with GitHub Actions Inactive
Vedaant-J added a commit that referenced this pull request Nov 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests November 2, 2026 19:54 — with GitHub Actions Inactive

@Victsm Victsm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two remaining data-integrity blockers. The chunked-filesystem implementation is unchanged from the independently reproduced version; the latest ADLS test does not cover these cases.

  1. Failed constructor: start with a.bin = b"preserve-me"; make _stream() raise while constructing _Writer(..., size=1). Finalization leaves a.bin = b"", because _failed is false and close() publishes the empty spool. Keep construction failed until fallible setup succeeds, clean up on failure, and test that finalization preserves the old bytes.
  2. Overlapping writers: with 16-byte parts, B writes 19 bytes; A commits the same destination and deletes B's unfinished first part; B writes 18 more bytes and commits. B's intended 37-byte file reads as 21 bytes despite _SUCCESS. Cleanup must preserve other unfinished/newer generations; readers should reject a committed part layout with gaps. Add that exact same-path regression.

These corroborate the two open bot findings: #112 (comment) and #112 (comment) . No new transactional filesystem subsystem is requested.

Approval prerequisites: fix both cases, pass the focused regressions plus the existing chunked-filesystem suite, propagate the correction through the stack, and complete required CI. The filesystem-only model/prediction adapter issues are already addressed; those are not additional open blockers.

Reviewed head: 3d3c9d896b8d3a0490f19864419457828c15dd91.

Vedaant-J and others added 2 commits November 2, 2026 20:07
…er publishes an empty file

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…parts are complete

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests November 2, 2026 20:09 — with GitHub Actions Inactive
Vedaant-J added a commit that referenced this pull request Nov 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vedaant-J added a commit that referenced this pull request Nov 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vedaant-J added a commit that referenced this pull request Nov 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	packages/kumo-engine/docs/mlops.md
Vedaant-J added a commit that referenced this pull request Nov 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	packages/kumo-engine/docs/mlops.md
Vedaant-J added a commit that referenced this pull request Nov 2, 2026
…tem chart into #115

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
gha-kumo-workflow-tests — 5f75e66a Deployed Nov 2, 2026 by Vedaant-J via import #1223
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants